@onekeyfe/react-native-pager-view 3.0.165 → 3.0.167

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.
@@ -1301,9 +1301,15 @@ class CollapsiblePagerHost(context: Context) : NestedScrollableHost(context), Ne
1301
1301
 
1302
1302
  MotionEvent.ACTION_MOVE -> {
1303
1303
  if (!headerTouchActive) return super.dispatchTouchEvent(event)
1304
+ val dx = event.x - headerDownX
1305
+ val dy = event.y - headerDownY
1306
+ // A horizontal header guard must not consume a later vertical drag.
1307
+ if (headerGestureOwner != HeaderGestureOwner.LIST &&
1308
+ kotlin.math.abs(dy) > touchSlopPx && kotlin.math.abs(dy) > kotlin.math.abs(dx)) {
1309
+ headerGestureOwner = HeaderGestureOwner.LIST
1310
+ log("direction-recover owner=list region=$headerTouchRegion dx=${dx.roundToInt()} dy=${dy.roundToInt()}")
1311
+ }
1304
1312
  if (headerGestureOwner == HeaderGestureOwner.NONE) {
1305
- val dx = event.x - headerDownX
1306
- val dy = event.y - headerDownY
1307
1313
  if (kotlin.math.abs(dx) <= touchSlopPx && kotlin.math.abs(dy) <= touchSlopPx) {
1308
1314
  return super.dispatchTouchEvent(event)
1309
1315
  }
@@ -1,5 +1,7 @@
1
1
  package com.reactnativepagerview
2
2
 
3
+ import com.margelo.nitro.nativelogger.OneKeyLog
4
+
3
5
  import android.content.Context
4
6
  import android.graphics.Color
5
7
  import android.os.SystemClock
@@ -380,6 +382,11 @@ internal class NativeScrollerViewport(context: Context, internal val host: Nativ
380
382
  private var stopping = false
381
383
  private var startingProgrammaticScroll = false
382
384
  private var downY = 0f
385
+ private var directionDownX = 0f
386
+ private var directionDownY = 0f
387
+ private var directionPointerId = MotionEvent.INVALID_POINTER_ID
388
+ private var smoothPagerTouch = false
389
+ private var touchAxis = 0
383
390
  private var velocityTracker: VelocityTracker? = null
384
391
  private var activePointerId = MotionEvent.INVALID_POINTER_ID
385
392
  private val maximumVelocity = ViewConfiguration.get(context).scaledMaximumFlingVelocity
@@ -391,16 +398,110 @@ internal class NativeScrollerViewport(context: Context, internal val host: Nativ
391
398
  overScrollMode = OVER_SCROLL_NEVER
392
399
  }
393
400
 
401
+ private fun updateTouchDirection(event: MotionEvent) {
402
+ if (event.actionMasked == MotionEvent.ACTION_DOWN) {
403
+ directionPointerId = event.getPointerId(0)
404
+ directionDownX = event.x
405
+ directionDownY = event.y
406
+ downY = event.y
407
+ touchAxis = 0
408
+ smoothPagerTouch = false
409
+ var ancestor = host.parent as? View
410
+ while (ancestor != null) {
411
+ if (ancestor is CollapsiblePagerHost && ancestor.nativeSmoothHeaderScrollEnabled) {
412
+ smoothPagerTouch = true
413
+ break
414
+ }
415
+ ancestor = ancestor.parent as? View
416
+ }
417
+ OneKeyLog.debug("NativeScroller", "viewport-down slop=$touchSlop density=${resources.displayMetrics.density} scrollY=$scrollY smooth=$smoothPagerTouch")
418
+ } else if (event.actionMasked == MotionEvent.ACTION_POINTER_UP &&
419
+ event.getPointerId(event.actionIndex) == directionPointerId) {
420
+ val replacement = if (event.actionIndex == 0) 1 else 0
421
+ directionPointerId = if (replacement < event.pointerCount) event.getPointerId(replacement)
422
+ else MotionEvent.INVALID_POINTER_ID
423
+ if (replacement < event.pointerCount) {
424
+ directionDownX = event.getX(replacement)
425
+ directionDownY = event.getY(replacement)
426
+ }
427
+ } else if (smoothPagerTouch && touchAxis == 0 && event.actionMasked == MotionEvent.ACTION_MOVE) {
428
+ val pointerIndex = event.findPointerIndex(directionPointerId)
429
+ if (pointerIndex < 0) return
430
+ val dx = abs(event.getX(pointerIndex) - directionDownX)
431
+ val dy = abs(event.getY(pointerIndex) - directionDownY)
432
+ if (dy > touchSlop && dy > dx) touchAxis = 1
433
+ else if (dx >= 32f * resources.displayMetrics.density && dx > dy) touchAxis = 2
434
+ if (touchAxis != 0) {
435
+ OneKeyLog.debug("NativeScroller", "viewport-direction axis=$touchAxis dx=$dx dy=$dy scrollY=$scrollY")
436
+ }
437
+ }
438
+ }
439
+
440
+ private var childDisallowsIntercept = false
441
+ private var loggedBlockedVertical = false
442
+
443
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean {
444
+ if (event.actionMasked == MotionEvent.ACTION_DOWN) {
445
+ childDisallowsIntercept = false
446
+ loggedBlockedVertical = false
447
+ }
448
+ if (scrollEnabled) updateTouchDirection(event)
449
+ if (smoothPagerTouch && touchAxis == 1 && childDisallowsIntercept &&
450
+ event.actionMasked == MotionEvent.ACTION_MOVE) {
451
+ childDisallowsIntercept = false
452
+ OneKeyLog.debug("NativeScroller", "viewport-recover-vertical scrollY=$scrollY")
453
+ super.requestDisallowInterceptTouchEvent(false)
454
+ }
455
+ if (smoothPagerTouch && event.actionMasked == MotionEvent.ACTION_MOVE && !loggedBlockedVertical) {
456
+ val pointerIndex = event.findPointerIndex(directionPointerId)
457
+ if (pointerIndex >= 0) {
458
+ val dx = abs(event.getX(pointerIndex) - directionDownX)
459
+ val dy = abs(event.getY(pointerIndex) - directionDownY)
460
+ if (dy > touchSlop && dy > dx) {
461
+ loggedBlockedVertical = true
462
+ OneKeyLog.debug("NativeScroller", "viewport-dispatch-vertical dx=$dx dy=$dy axis=$touchAxis disallow=$childDisallowsIntercept scrollY=$scrollY")
463
+ }
464
+ }
465
+ }
466
+ try { return super.dispatchTouchEvent(event) } finally {
467
+ if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) {
468
+ releaseTouchDirection()
469
+ }
470
+ }
471
+ }
472
+
473
+ override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
474
+ childDisallowsIntercept = disallowIntercept
475
+ if (smoothPagerTouch) OneKeyLog.debug("NativeScroller", "viewport-child-disallow value=$disallowIntercept axis=$touchAxis scrollY=$scrollY")
476
+ super.requestDisallowInterceptTouchEvent(disallowIntercept)
477
+ }
478
+
394
479
  override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
395
480
  if (!scrollEnabled) return false
396
- if (event.actionMasked == MotionEvent.ACTION_DOWN) downY = event.y
481
+ updateTouchDirection(event)
482
+ if (smoothPagerTouch && event.actionMasked == MotionEvent.ACTION_MOVE && touchAxis != 1) return false
397
483
  val intercepted = super.onInterceptTouchEvent(event)
398
- if (intercepted && event.actionMasked == MotionEvent.ACTION_MOVE) beginDrag(event)
484
+ if (intercepted && event.actionMasked == MotionEvent.ACTION_MOVE) {
485
+ OneKeyLog.debug("NativeScroller", "viewport-intercept dy=${event.y - downY} slop=$touchSlop scrollY=$scrollY")
486
+ beginDrag(event)
487
+ }
399
488
  return intercepted
400
489
  }
401
490
 
402
491
  override fun onTouchEvent(event: MotionEvent): Boolean {
403
492
  if (!scrollEnabled) return false
493
+ updateTouchDirection(event)
494
+ if (smoothPagerTouch && touchAxis != 1) {
495
+ if (event.actionMasked == MotionEvent.ACTION_MOVE) return true
496
+ if (event.actionMasked == MotionEvent.ACTION_UP) {
497
+ val cancel = MotionEvent.obtain(event)
498
+ cancel.action = MotionEvent.ACTION_CANCEL
499
+ try { return super.onTouchEvent(cancel) } finally {
500
+ cancel.recycle()
501
+ releaseTouchDirection()
502
+ }
503
+ }
504
+ }
404
505
  when (event.actionMasked) {
405
506
  MotionEvent.ACTION_DOWN -> downY = event.y
406
507
  MotionEvent.ACTION_MOVE -> {
@@ -409,7 +510,19 @@ internal class NativeScrollerViewport(context: Context, internal val host: Nativ
409
510
  }
410
511
  MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> endDrag(event)
411
512
  }
412
- return super.onTouchEvent(event)
513
+ try { return super.onTouchEvent(event) } finally {
514
+ if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) {
515
+ releaseTouchDirection()
516
+ }
517
+ }
518
+ }
519
+
520
+ private fun releaseTouchDirection() {
521
+ childDisallowsIntercept = false
522
+ loggedBlockedVertical = false
523
+ directionPointerId = MotionEvent.INVALID_POINTER_ID
524
+ smoothPagerTouch = false
525
+ touchAxis = 0
413
526
  }
414
527
 
415
528
  // Record at the refresh boundary so interception does not truncate the pointer history.
@@ -435,6 +548,7 @@ internal class NativeScrollerViewport(context: Context, internal val host: Nativ
435
548
  }
436
549
 
437
550
  internal fun releaseTouchTracking() {
551
+ releaseTouchDirection()
438
552
  velocityTracker?.recycle()
439
553
  velocityTracker = null
440
554
  activePointerId = MotionEvent.INVALID_POINTER_ID
@@ -443,6 +557,7 @@ internal class NativeScrollerViewport(context: Context, internal val host: Nativ
443
557
  internal fun beginDrag(event: MotionEvent) {
444
558
  if (dragging) return
445
559
  dragging = true
560
+ OneKeyLog.debug("NativeScroller", "viewport-begin-drag dy=${event.y - downY} slop=$touchSlop scrollY=$scrollY")
446
561
  // Header-forwarded touches bypass the refresh boundary's touch stream.
447
562
  host.cancelPendingScrollRestore()
448
563
  if (host.keyboardDismissMode != "none") host.dismissKeyboard()
@@ -543,6 +658,8 @@ private class NativeScrollerRefreshLayout(context: Context) : SwipeRefreshLayout
543
658
  private var downX = 0f
544
659
  private var downY = 0f
545
660
  private var horizontal = false
661
+ private var vertical = false
662
+ private var smoothPagerGesture = false
546
663
  private var intercepted = false
547
664
  private val slop = ViewConfiguration.get(context).scaledTouchSlop
548
665
 
@@ -567,10 +684,26 @@ private class NativeScrollerRefreshLayout(context: Context) : SwipeRefreshLayout
567
684
  downX = event.x
568
685
  downY = event.y
569
686
  horizontal = false
687
+ vertical = false
688
+ smoothPagerGesture = false
689
+ var ancestor = parent as? View
690
+ while (ancestor != null) {
691
+ if (ancestor is CollapsiblePagerHost && ancestor.nativeSmoothHeaderScrollEnabled) {
692
+ smoothPagerGesture = true
693
+ break
694
+ }
695
+ ancestor = ancestor.parent as? View
696
+ }
570
697
  intercepted = false
571
- } else if (event.actionMasked == MotionEvent.ACTION_MOVE && abs(event.x - downX) > slop &&
572
- abs(event.x - downX) > abs(event.y - downY)) {
573
- horizontal = true
698
+ } else if (event.actionMasked == MotionEvent.ACTION_MOVE && (!smoothPagerGesture || !vertical)) {
699
+ val dx = abs(event.x - downX)
700
+ val dy = abs(event.y - downY)
701
+ if (smoothPagerGesture && dy > slop && dy > dx) {
702
+ vertical = true
703
+ horizontal = false
704
+ } else if (dx > (if (smoothPagerGesture) 32f * resources.displayMetrics.density else slop.toFloat()) && dx > dy) {
705
+ horizontal = true
706
+ }
574
707
  }
575
708
  if (horizontal) return false
576
709
  val result = super.onInterceptTouchEvent(event)
@@ -5,11 +5,15 @@ import android.util.AttributeSet
5
5
  import android.view.Choreographer
6
6
  import android.view.MotionEvent
7
7
  import android.view.View
8
+ import android.view.ViewGroup
8
9
  import android.view.ViewConfiguration
10
+ import android.widget.HorizontalScrollView
9
11
  import android.widget.FrameLayout
12
+ import androidx.recyclerview.widget.RecyclerView
10
13
  import androidx.viewpager2.widget.ViewPager2
11
14
  import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
12
15
  import com.facebook.react.uimanager.events.NativeGestureUtil
16
+ import com.margelo.nitro.nativelogger.OneKeyLog
13
17
  import kotlin.math.absoluteValue
14
18
  import kotlin.math.sign
15
19
 
@@ -32,6 +36,11 @@ open class NestedScrollableHost : FrameLayout {
32
36
  private var touchSlop = 0
33
37
  private var initialX = 0f
34
38
  private var initialY = 0f
39
+ private var smoothTouchActive = false
40
+ private var smoothTouchAxis = 0
41
+ private var smoothDownX = 0f
42
+ private var smoothDownY = 0f
43
+ private var nestedHorizontalOwner: View? = null
35
44
  protected var nestedNativeGestureStarted: Boolean = false
36
45
  private val parentViewPager: ViewPager2?
37
46
  get() {
@@ -57,8 +66,74 @@ open class NestedScrollableHost : FrameLayout {
57
66
  }
58
67
  }
59
68
 
69
+ private fun hasSmoothHeaderOwner(): Boolean {
70
+ var ancestor: View? = this
71
+ while (ancestor != null) {
72
+ if (ancestor is CollapsiblePagerHost && ancestor.nativeSmoothHeaderScrollEnabled) return true
73
+ ancestor = ancestor.parent as? View
74
+ }
75
+ return false
76
+ }
77
+
78
+ private fun horizontalOwnerAt(view: View, x: Float, y: Float): View? {
79
+ if (view is ViewGroup) {
80
+ for (index in view.childCount - 1 downTo 0) {
81
+ val nested = view.getChildAt(index)
82
+ if (nested.visibility != View.VISIBLE || nested.alpha <= 0f) continue
83
+ val localX = x + view.scrollX - nested.left - nested.translationX
84
+ val localY = y + view.scrollY - nested.top - nested.translationY
85
+ if (localX < 0 || localY < 0 || localX >= nested.width || localY >= nested.height) continue
86
+ horizontalOwnerAt(nested, localX, localY)?.let { return it }
87
+ break
88
+ }
89
+ }
90
+ // Exclude this host's own pager; only an inner horizontal viewport has priority.
91
+ if (view.parent !== child && (view is HorizontalScrollView || view is RecyclerView) &&
92
+ (view.canScrollHorizontally(-1) || view.canScrollHorizontally(1))) return view
93
+ return null
94
+ }
95
+
96
+ private fun coordinateSmoothTouch(e: MotionEvent) {
97
+ val pager = child as? ViewPager2 ?: return
98
+ if (e.actionMasked == MotionEvent.ACTION_DOWN) {
99
+ smoothTouchActive = hasSmoothHeaderOwner()
100
+ smoothTouchAxis = 0
101
+ smoothDownX = e.x
102
+ smoothDownY = e.y
103
+ nestedHorizontalOwner = if (smoothTouchActive) horizontalOwnerAt(this, e.x, e.y) else null
104
+ }
105
+ if (!smoothTouchActive) return
106
+ val dx = e.x - smoothDownX
107
+ val dy = e.y - smoothDownY
108
+ if (e.actionMasked == MotionEvent.ACTION_MOVE && smoothTouchAxis == 0) {
109
+ smoothTouchAxis = when {
110
+ dy.absoluteValue > touchSlop && dy.absoluteValue > dx.absoluteValue -> 1
111
+ dx.absoluteValue >= 32f * resources.displayMetrics.density && dx.absoluteValue > dy.absoluteValue -> 2
112
+ else -> 0
113
+ }
114
+ if (smoothTouchAxis != 0) {
115
+ OneKeyLog.debug("CollapsiblePager", "content-direction owner=${if (smoothTouchAxis == 1) "list" else "horizontal"} dx=$dx dy=$dy nested=${nestedHorizontalOwner != null}")
116
+ }
117
+ }
118
+ val innerCanScroll = nestedHorizontalOwner?.canScrollHorizontally(-dx.sign.toInt()) == true
119
+ // Preserve DOWN in the real pager, but defer horizontal interception while
120
+ // intent is pending. Ancestor vertical lists must still be able to intercept.
121
+ (pager.getChildAt(0) as? RecyclerView)?.requestDisallowInterceptTouchEvent(smoothTouchAxis != 2 || innerCanScroll)
122
+ parent?.requestDisallowInterceptTouchEvent(false)
123
+ }
124
+
60
125
  override fun dispatchTouchEvent(e: MotionEvent): Boolean {
126
+ coordinateSmoothTouch(e)
61
127
  val handled = super.dispatchTouchEvent(e)
128
+ if (smoothTouchActive && (e.actionMasked == MotionEvent.ACTION_UP || e.actionMasked == MotionEvent.ACTION_CANCEL)) {
129
+ if (nestedNativeGestureStarted) {
130
+ NativeGestureUtil.notifyNativeGestureEnded(this, e)
131
+ nestedNativeGestureStarted = false
132
+ }
133
+ smoothTouchActive = false
134
+ smoothTouchAxis = 0
135
+ nestedHorizontalOwner = null
136
+ }
62
137
 
63
138
  // After the full child dispatch cycle, a deeply-nested NestedScrollableHost may
64
139
  // have called requestDisallowInterceptTouchEvent(false) which propagates all the
package/docs/SPEC.md CHANGED
@@ -187,6 +187,72 @@ unchanged. Acceptance requires a real iOS button-origin drag that does not fire
187
187
  the button, a button-origin fling that scrolls, an ordinary tap that fires, and
188
188
  focused native touch-delivery coverage.
189
189
 
190
+ ## Smooth-header touch direction and press retention
191
+
192
+
193
+ Status: implemented in module source. Equivalent native gesture behavior was
194
+ independently verified in the Home consumer's recorded round3 simulator cases.
195
+ The module transplant preserves those decisions and excludes application-specific
196
+ acceptance probes. Package compilation and packaging are checked separately;
197
+ consumer evidence does not establish acceptance of a subsequently installed
198
+ release. Physical-device feel and semantic accessibility activation remain
199
+ unverified. No public API is added.
200
+
201
+ Existing smooth-header callers MUST receive this behavior automatically, with no
202
+ additional caller options.
203
+
204
+ On iOS and Android, smooth-header mode MUST distinguish pending intent from a
205
+ confirmed vertical or horizontal drag. A small horizontal start followed by
206
+ vertical movement MUST be allowed to become a vertical scroll while intent is
207
+ pending. A confirmed vertical drag MUST retain vertical ownership until end or
208
+ cancellation, even if later movement becomes horizontal. This applies to page
209
+ content, the shared header, and touches beginning on an inner horizontal banner.
210
+ Normal taps MUST remain functional. Press-retention behavior in application
211
+ controls belongs to the consuming responder implementation; the Pager package
212
+ does not contain or publish Tamagui helpers. The native application tab bar has
213
+ a separate contract in react-native-tab-view/docs/SPEC.md.
214
+
215
+ The current native implementation confirms horizontal intent after at least
216
+ 32 points on iOS or 32 dp on Android, with horizontal displacement exceeding
217
+ vertical displacement. Vertical intent uses the native vertical scroll threshold
218
+ (Android touch slop; iOS guard 10 points) and vertical-dominant displacement.
219
+ There is no timed wait and no percentage-of-page-width threshold. After horizontal
220
+ intent is confirmed, page content may retain horizontal ownership: the contract
221
+ does not require transferring an already established page swipe to a vertical
222
+ scroller. Header forwarding can recover to the vertical list when vertical
223
+ movement becomes dominant. An inner horizontal viewport that can consume a
224
+ confirmed horizontal drag MUST have priority over the Home page pager.
225
+
226
+ This intent confirmation MUST NOT add a second lazy-mount progress threshold.
227
+ Once native horizontal dragging begins, the first nonzero page progress MUST
228
+ prepare the target page on both platforms, as specified below.
229
+
230
+ Acceptance MUST cover an initial small horizontal movement followed by vertical
231
+ scrolling from ordinary content, header buttons, Prime/support controls and inner
232
+ banners; vertical-first gestures that later turn horizontal; stationary taps;
233
+ normal page swipes; inner banner swipes; and iOS fixed-control drag cancellation.
234
+ Native logger traces and before/after frames MUST distinguish scrolling, unintended
235
+ page changes and unintended control activation. Simulator acceptance establishes only the recorded touch sequences; it does not
236
+ establish physical-device feel or untested multi-touch combinations.
237
+
238
+ On Smooth Home, Android NativeScrollerViewport MUST use the same pending /
239
+ vertical / horizontal direction decision on interception and on its own touch
240
+ path. It MUST deliver the original DOWN to NestedScrollView, defer pending and
241
+ horizontal MOVE scrolling, and retain confirmed vertical ownership until the
242
+ sequence ends. Header-forwarded DOWN MUST initialize this decision independently
243
+ of the refresh boundary. Dispatch MUST still observe child-owned movement; only
244
+ a confirmed vertical Smooth Home gesture may release the viewport's child
245
+ interception prohibition. Pending and horizontal movement MUST preserve it.
246
+
247
+ iOS nested horizontal ownership MUST include enabled nonpaging horizontal-only
248
+ ScrollViews, with content wider than bounds and no vertical content overflow.
249
+ Both pan holding and failure dependencies MUST use the same eligibility. A
250
+ failure dependency on the Pager guard MUST apply only to the horizontal pan;
251
+ independent non-pan recognizers, including stationary native long presses, MUST
252
+ remain able to recognize before finger-up while Pager intent is pending. A
253
+ second touch in the pending Pager guard fails recognition; an already active
254
+ Pager guard cancels and restores each held pan's original minimum touch count.
255
+
190
256
  ## Native tab-press animation default
191
257
 
192
258
  Behavior change: previously an omitted `nativeTabPressAnimationEnabled` animated
@@ -523,3 +589,55 @@ introduced. This change makes no frame-rate claim.
523
589
  The newly merged NativeScroller viewport tracking uses the same weak-key
524
590
  snapshot rule. Its cleanup has source and focused regression coverage; the
525
591
  pre-migration runtime checks above do not establish its device acceptance.
592
+
593
+ ## iOS lazy-page first-frame preparation
594
+
595
+ Status: implemented and runtime verified in the consuming app's local native
596
+ build on iOS 26.5 (2026-10-10). This package change preserves the same functional
597
+ source; app-specific diagnostic logs remain in that local build.
598
+
599
+ In smooth-header mode, a newly mounted, owned NativeScroller or live NativeList
600
+ on the current page, either adjacent page, or a programmatic destination MUST
601
+ receive its pager insets and restored offset in Fabric's did-mount callback,
602
+ before the next frame. This also applies during a horizontal drag or animation.
603
+ Preparation MUST NOT change the selected page, move the shared header out of its
604
+ transition owner, attach an offscreen page's observer, or repeatedly restore an
605
+ already managed scroll view.
606
+
607
+ A scroll view removed in a Fabric transaction MUST be excluded from discovery
608
+ until that transaction finishes. The exclusion uses weak native identities,
609
+ is cleared after did-mount and on recycle, and allows a moved view to be found
610
+ again in its next owner. A NativeList collection is usable only while its nearest
611
+ Fabric component owns it through contentView, has a registered tag, and retains
612
+ that content view as collection delegate. A disposed or unregistered NativeList
613
+ MUST NOT become a generic collection fallback.
614
+
615
+ Verified NativeList collections bypass the existing 0.5-second generic collection
616
+ fallback debounce, including replacement-state restoration. Other collection
617
+ fallbacks keep that safety delay. Caller insets and retained logical offsets
618
+ remain governed by the existing restoration policy.
619
+
620
+ This is a UI-thread, main-runtime native coordination change with no new props,
621
+ events, background-runtime work, persistence or retained strong view cache.
622
+ Android already prepares discovered scroll children before draw and is unchanged;
623
+ Web is unchanged. Consumers own the decision to lazily mount or prepare a target
624
+ page; this package does not import scene code or start business requests.
625
+
626
+ The Home consumer uses the same lazy-mount trigger on iOS and Android: after
627
+ entering the horizontal dragging state, the first received page-scroll progress
628
+ with nonzero displacement relative to the starting page MUST trigger early
629
+ mounting of the adjacent target page in that direction. There is no fixed
630
+ distance or percentage threshold. Measured trigger percentages, such as 0.65%,
631
+ are event samples rather than configured thresholds. Early mounting MUST NOT
632
+ change the selected page; selection follows the existing native selection event.
633
+
634
+ Conformance: RNCCollapsiblePagerViewComponentView.mm and the focused ownership /
635
+ first-frame XCTest cases. Consumer acceptance confirmed a cold Spot-to-Perps
636
+ swipe: the native skeleton received inset 340 and offset -340 at t=1791603349124
637
+ while transition=1, 427 ms before native drag settlement. Its window y=514 was
638
+ below the header bottom y=502. These values are measured evidence, not package
639
+ constants. Native view geometry is confirmed. Later finite consumer round3 simulator
640
+ cases additionally verified recorded refresh, header, restoration and gesture
641
+ paths; they do not establish every possible sequence or runtime acceptance of a
642
+ newly installed module release. Physical-device feel and semantic accessibility
643
+ activation remain unverified.
@@ -158,6 +158,28 @@ static UIView *RNCScrollComponentView(UIScrollView *scrollView)
158
158
  return nil;
159
159
  }
160
160
 
161
+ // NativeList owns its collection through a Fabric contentView and its delegate.
162
+ // A dropped Nitro view can remain in the hierarchy after its delegate is cleared.
163
+ static RCTViewComponentView *RNCNativeListComponentForScrollView(UIScrollView *scrollView)
164
+ {
165
+ Class nativeListClass = NSClassFromString(@"HybridNativeListComponent");
166
+ for (UIView *component = scrollView; component != nil; component = component.superview) {
167
+ if (![component isKindOfClass:RCTViewComponentView.class]) continue;
168
+ return nativeListClass != Nil && [component isKindOfClass:nativeListClass]
169
+ ? (RCTViewComponentView *)component : nil;
170
+ }
171
+ return nil;
172
+ }
173
+
174
+ static BOOL RNCIsMountedNativeListScrollView(UIScrollView *scrollView)
175
+ {
176
+ if (![scrollView isKindOfClass:UICollectionView.class]) return NO;
177
+ RCTViewComponentView *component = RNCNativeListComponentForScrollView(scrollView);
178
+ UIView *content = component.contentView;
179
+ return component.tag > 0 && content != nil && [scrollView isDescendantOfView:content] &&
180
+ ((UICollectionView *)scrollView).delegate == (id)content;
181
+ }
182
+
161
183
  typedef void (^RNCCollapsiblePagerNativeTabPressHandler)(NSInteger index, NSString *key);
162
184
 
163
185
  static CGFloat RNCClamp(CGFloat value, CGFloat minimum, CGFloat maximum)
@@ -165,6 +187,24 @@ static CGFloat RNCClamp(CGFloat value, CGFloat minimum, CGFloat maximum)
165
187
  return MIN(MAX(value, minimum), maximum);
166
188
  }
167
189
 
190
+ static BOOL RNCIsEnabledHorizontalScrollOwner(UIScrollView *scroll)
191
+ {
192
+ return scroll.scrollEnabled && scroll.panGestureRecognizer.enabled &&
193
+ scroll.contentSize.width > CGRectGetWidth(scroll.bounds) && !scroll.alwaysBounceVertical &&
194
+ scroll.contentSize.height <= CGRectGetHeight(scroll.bounds) + 0.5;
195
+ }
196
+
197
+ static void RNCGuardNestedHorizontalPans(UIView *view, UIGestureRecognizer *guard)
198
+ {
199
+ if ([view isKindOfClass:UIScrollView.class]) {
200
+ UIScrollView *scroll = (UIScrollView *)view;
201
+ if (RNCIsEnabledHorizontalScrollOwner(scroll)) {
202
+ [scroll.panGestureRecognizer requireGestureRecognizerToFail:guard];
203
+ }
204
+ }
205
+ for (UIView *child in view.subviews) RNCGuardNestedHorizontalPans(child, guard);
206
+ }
207
+
168
208
  static void RNCCollapsiblePagerLog(NSString *message)
169
209
  {
170
210
  Class logClass = NSClassFromString(@"ReactNativeNativeLogger.OneKeyLog");
@@ -995,6 +1035,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
995
1035
  intent.y]);
996
1036
  return NO;
997
1037
  }
1038
+ UIView *target = [self hitTest:[pan locationInView:self] withEvent:nil];
1039
+ for (UIView *ancestor = target; ancestor != nil && ancestor != self; ancestor = ancestor.superview) {
1040
+ if (![ancestor isKindOfClass:UIScrollView.class]) continue;
1041
+ UIScrollView *inner = (UIScrollView *)ancestor;
1042
+ CGFloat maximum = MAX(0, inner.contentSize.width - CGRectGetWidth(inner.bounds));
1043
+ BOOL canScroll = maximum > 0 && (intent.x < 0
1044
+ ? inner.contentOffset.x < maximum - 0.5 : inner.contentOffset.x > 0.5);
1045
+ if (canScroll) return NO;
1046
+ }
998
1047
  }
999
1048
  if (gestureRecognizer == self.panGestureRecognizer && self.excludedHeaderView != nil) {
1000
1049
  CGPoint point = [gestureRecognizer locationInView:self.excludedHeaderView];
@@ -1187,16 +1236,134 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1187
1236
 
1188
1237
  @end
1189
1238
 
1190
- @interface RNCCollapsiblePagerVerticalPagerGuardGestureRecognizer : UIPanGestureRecognizer
1239
+ @interface RNCCollapsiblePagerVerticalPagerGuardGestureRecognizer : UIGestureRecognizer
1240
+ @property (nonatomic, weak) UIPanGestureRecognizer *pagerPan;
1241
+ - (CGPoint)translationInView:(UIView *)view;
1242
+ - (CGPoint)velocityInView:(UIView *)view;
1191
1243
  @end
1192
1244
 
1193
- @implementation RNCCollapsiblePagerVerticalPagerGuardGestureRecognizer
1245
+ @implementation RNCCollapsiblePagerVerticalPagerGuardGestureRecognizer {
1246
+ __weak UITouch *_trackedTouch;
1247
+ NSMapTable<UIPanGestureRecognizer *, NSNumber *> *_heldPans;
1248
+ CGPoint _startPoint;
1249
+ CGPoint _lastPoint;
1250
+ CGPoint _velocity;
1251
+ NSTimeInterval _lastTimestamp;
1252
+ }
1194
1253
 
1195
- - (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer
1254
+ - (void)holdPan:(UIPanGestureRecognizer *)pan
1196
1255
  {
1197
- return NO;
1256
+ if (pan == nil || [_heldPans objectForKey:pan] != nil) return;
1257
+ if (_heldPans == nil) _heldPans = [NSMapTable weakToStrongObjectsMapTable];
1258
+ [_heldPans setObject:@(pan.minimumNumberOfTouches) forKey:pan];
1259
+ // A failure dependency alone lets UIKit's pager pan cancel a descendant pan
1260
+ // before intent resolves. Keep DOWN tracked while deferring one-finger begin.
1261
+ pan.minimumNumberOfTouches = MAX(2, pan.minimumNumberOfTouches);
1262
+ }
1263
+
1264
+ - (void)restoreHeldPans
1265
+ {
1266
+ for (UIPanGestureRecognizer *pan in _heldPans.keyEnumerator) {
1267
+ pan.minimumNumberOfTouches = [_heldPans objectForKey:pan].unsignedIntegerValue;
1268
+ UIScrollView *scroll = [pan.view isKindOfClass:UIScrollView.class] ? (UIScrollView *)pan.view : nil;
1269
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
1270
+ @"gesture-restore pan=%@ min-touches=%lu offset=%@",
1271
+ NSStringFromClass(pan.view.class), (unsigned long)pan.minimumNumberOfTouches,
1272
+ scroll != nil ? NSStringFromCGPoint(scroll.contentOffset) : @"none"]);
1273
+ }
1274
+ [_heldPans removeAllObjects];
1275
+ }
1276
+
1277
+ - (CGPoint)translationInView:(UIView *)view
1278
+ {
1279
+ CGPoint start = [view convertPoint:_startPoint fromView:self.view];
1280
+ CGPoint last = [view convertPoint:_lastPoint fromView:self.view];
1281
+ return CGPointMake(last.x - start.x, last.y - start.y);
1282
+ }
1283
+
1284
+ - (CGPoint)velocityInView:(UIView *)view { return _velocity; }
1285
+
1286
+ - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
1287
+ {
1288
+ if (_trackedTouch != nil || touches.count != 1) {
1289
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
1290
+ @"gesture-second-touch previous-state=%ld incoming=%lu held-pans=%lu",
1291
+ (long)self.state, (unsigned long)touches.count, (unsigned long)_heldPans.count]);
1292
+ [self restoreHeldPans];
1293
+ if (self.state == UIGestureRecognizerStatePossible) {
1294
+ self.state = UIGestureRecognizerStateFailed;
1295
+ } else if (self.state == UIGestureRecognizerStateBegan || self.state == UIGestureRecognizerStateChanged) {
1296
+ self.state = UIGestureRecognizerStateCancelled;
1297
+ }
1298
+ return;
1299
+ }
1300
+ _trackedTouch = touches.anyObject;
1301
+ _startPoint = _lastPoint = [touches.anyObject locationInView:self.view];
1302
+ _lastTimestamp = event.timestamp;
1303
+ _velocity = CGPointZero;
1304
+ [self holdPan:self.pagerPan];
1305
+ for (UIView *view = _trackedTouch.view; view != nil && view != self.view; view = view.superview) {
1306
+ if (![view isKindOfClass:UIScrollView.class]) continue;
1307
+ UIScrollView *scroll = (UIScrollView *)view;
1308
+ if (RNCIsEnabledHorizontalScrollOwner(scroll)) {
1309
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
1310
+ @"gesture-hold inner=%@ paging=%d offset=%@ content=%@ bounds=%@",
1311
+ NSStringFromClass(scroll.class), scroll.pagingEnabled, NSStringFromCGPoint(scroll.contentOffset),
1312
+ NSStringFromCGSize(scroll.contentSize), NSStringFromCGSize(scroll.bounds.size)]);
1313
+ [self holdPan:scroll.panGestureRecognizer];
1314
+ }
1315
+ }
1316
+ }
1317
+
1318
+ - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
1319
+ {
1320
+ CGPoint point = [touches.anyObject locationInView:self.view];
1321
+ NSTimeInterval elapsed = event.timestamp - _lastTimestamp;
1322
+ if (elapsed > 0) {
1323
+ _velocity = CGPointMake((point.x - _lastPoint.x) / elapsed,
1324
+ (point.y - _lastPoint.y) / elapsed);
1325
+ }
1326
+ _lastPoint = point;
1327
+ _lastTimestamp = event.timestamp;
1328
+ if (self.state == UIGestureRecognizerStatePossible) {
1329
+ CGFloat dx = fabs(point.x - _startPoint.x), dy = fabs(point.y - _startPoint.y);
1330
+ // Keep intent pending until the explicit distance decision, instead of
1331
+ // relying on UIPanGestureRecognizer's earlier recognition.
1332
+ if ((dy >= 10 && dy > dx) || (dx >= 32 && dx > dy)) {
1333
+ RNCCollapsiblePagerLog([NSString stringWithFormat:
1334
+ @"gesture-intent axis=%@ dx=%.2f dy=%.2f held-pans=%lu",
1335
+ dy > dx ? @"vertical" : @"horizontal", dx, dy, (unsigned long)_heldPans.count]);
1336
+ self.state = UIGestureRecognizerStateBegan;
1337
+ if (self.state == UIGestureRecognizerStateFailed) [self restoreHeldPans];
1338
+ }
1339
+ } else if (self.state == UIGestureRecognizerStateBegan || self.state == UIGestureRecognizerStateChanged) {
1340
+ self.state = UIGestureRecognizerStateChanged;
1341
+ }
1342
+ }
1343
+
1344
+ - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
1345
+ {
1346
+ [self restoreHeldPans];
1347
+ self.state = self.state == UIGestureRecognizerStatePossible
1348
+ ? UIGestureRecognizerStateFailed : UIGestureRecognizerStateEnded;
1198
1349
  }
1199
1350
 
1351
+ - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
1352
+ {
1353
+ [self restoreHeldPans];
1354
+ self.state = UIGestureRecognizerStateCancelled;
1355
+ }
1356
+
1357
+ - (void)reset
1358
+ {
1359
+ [self restoreHeldPans];
1360
+ [super reset];
1361
+ _trackedTouch = nil;
1362
+ _velocity = CGPointZero;
1363
+ }
1364
+
1365
+ - (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)other { return NO; }
1366
+ - (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)other { return YES; }
1200
1367
  @end
1201
1368
 
1202
1369
  @interface RNCCollapsiblePagerViewComponentView () <
@@ -1282,10 +1449,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1282
1449
  NSMapTable<UIScrollView *, NSNumber *> *_appliedTopInsets;
1283
1450
  NSMapTable<UIScrollView *, NSValue *> *_originalIndicatorInsets;
1284
1451
  NSMapTable<UIScrollView *, NSNumber *> *_originalAlwaysBounceVertical;
1452
+ NSMapTable<UIScrollView *, NSNumber *> *_originalDirectionalLockEnabled;
1285
1453
  NSMapTable<UIScrollView *, NSNumber *> *_originalInsetAdjustmentBehavior;
1286
1454
  // OneKey patch: pair pre-mount release with recovery, including deferred mounts.
1287
1455
  NSMutableDictionary<NSString *, RNCCollapsiblePagerReleasedScrollState *> *_releasedPageScrollStates;
1288
1456
  BOOL _needsScrollObserverReattachAfterMount;
1457
+ NSHashTable<UIScrollView *> *_mountingRemovedScrollViews;
1289
1458
  __weak UIScrollView *_observedScrollView;
1290
1459
  __weak UIScrollView *_sharedHeaderScrollView;
1291
1460
  __weak UIScrollView *_pendingFallbackScrollView;
@@ -1294,7 +1463,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1294
1463
  UIPanGestureRecognizer *_sharedHeaderPressCancellationGesture;
1295
1464
  RNCCollapsiblePagerContentPressCancellationGestureRecognizer *_contentPressCancellationGesture;
1296
1465
  RNCCollapsiblePagerOuterPagerPanGestureRecognizer *_sharedHeaderOuterPagerGesture;
1297
- UIPanGestureRecognizer *_verticalPagerGesture;
1466
+ RNCCollapsiblePagerVerticalPagerGuardGestureRecognizer *_verticalPagerGesture;
1298
1467
  BOOL _observingContentOffset;
1299
1468
  BOOL _scrollResolveRetryScheduled;
1300
1469
  CFTimeInterval _pendingFallbackScrollViewDetectedAt;
@@ -1322,12 +1491,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1322
1491
  _pageControllers = [NSMutableArray new];
1323
1492
  _pageOffsets = [NSMutableDictionary new];
1324
1493
  _releasedPageScrollStates = [NSMutableDictionary new];
1494
+ _mountingRemovedScrollViews = [NSHashTable weakObjectsHashTable];
1325
1495
  _originalInsets = [NSMapTable weakToStrongObjectsMapTable];
1326
1496
  _managedRefreshControls = [NSMapTable weakToWeakObjectsMapTable];
1327
1497
  _refreshControlObservedScrollViews = [NSHashTable weakObjectsHashTable];
1328
1498
  _appliedTopInsets = [NSMapTable weakToStrongObjectsMapTable];
1329
1499
  _originalIndicatorInsets = [NSMapTable weakToStrongObjectsMapTable];
1330
1500
  _originalAlwaysBounceVertical = [NSMapTable weakToStrongObjectsMapTable];
1501
+ _originalDirectionalLockEnabled = [NSMapTable weakToStrongObjectsMapTable];
1331
1502
  _originalInsetAdjustmentBehavior = [NSMapTable weakToStrongObjectsMapTable];
1332
1503
  _pageKeys = @[];
1333
1504
  _retainedPages = @"[]";
@@ -1365,7 +1536,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1365
1536
  _verticalPagerGesture.cancelsTouchesInView = NO;
1366
1537
  _verticalPagerGesture.enabled = NO;
1367
1538
  [self addGestureRecognizer:_verticalPagerGesture];
1368
-
1369
1539
  _nativeTabBarView = [RNCCollapsiblePagerNativeTabBarView new];
1370
1540
  _nativeTabBarView.hidden = YES;
1371
1541
  __weak __typeof__(self) weakSelf = self;
@@ -1758,6 +1928,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1758
1928
  }
1759
1929
  break;
1760
1930
  }
1931
+ // Quarantine only this mount transaction; moved views may be valid again.
1932
+ [_mountingRemovedScrollViews addObject:scrollView];
1761
1933
  _needsScrollObserverReattachAfterMount = YES;
1762
1934
  [self restoreScrollViewState:scrollView resetSavedOffset:!belongsToPage];
1763
1935
  break;
@@ -1768,9 +1940,25 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
1768
1940
  - (void)mountingTransactionDidMount:(const MountingTransaction &)transaction
1769
1941
  withSurfaceTelemetry:(const SurfaceTelemetry &)surfaceTelemetry
1770
1942
  {
1771
- if (!_nativeSmoothHeaderScrollEnabled || _transitioning ||
1772
- _isPagerDragging || _isBeingRecycled || self.window == nil ||
1943
+ [_mountingRemovedScrollViews removeAllObjects];
1944
+ if (!_nativeSmoothHeaderScrollEnabled || _isBeingRecycled || self.window == nil ||
1773
1945
  _currentIndex < 0 || _currentIndex >= _pageControllers.count) return;
1946
+ // Prepare newly mounted owned scrollers before their first frame, even while
1947
+ // horizontal selection still owns the shared header and observer.
1948
+ NSInteger first = MAX(0, _currentIndex - 1);
1949
+ NSInteger last = MIN((NSInteger)_pageControllers.count - 1, _currentIndex + 1);
1950
+ for (NSInteger index = 0; index < _pageControllers.count; index++) {
1951
+ if ((index < first || index > last) && index != _destinationIndex) continue;
1952
+ UIScrollView *scrollView = [self verticalScrollViewForPageAtIndex:index];
1953
+ BOOL isOwnedScroller = [scrollView.superview isKindOfClass:RNCNativeScrollerComponentView.class] &&
1954
+ ((RNCNativeScrollerComponentView *)scrollView.superview).scrollView == scrollView &&
1955
+ [scrollView isDescendantOfView:_pageControllers[index].view];
1956
+ if ((RNCIsMountedNativeListScrollView(scrollView) || isOwnedScroller) &&
1957
+ [_originalInsets objectForKey:scrollView] == nil) {
1958
+ [self applyInsetsToScrollView:scrollView pageIndex:index restore:YES];
1959
+ }
1960
+ }
1961
+ if (_transitioning || _isPagerDragging) return;
1774
1962
 
1775
1963
  // OneKey patch: a release must recover even when the changed parent is above
1776
1964
  // the page subtree. Missing lists keep their identity for a later insertion.
@@ -2002,6 +2190,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2002
2190
  {
2003
2191
  _isBeingRecycled = YES;
2004
2192
  _generation++;
2193
+ [_mountingRemovedScrollViews removeAllObjects];
2005
2194
  _needsScrollObserverReattachAfterMount = NO;
2006
2195
  [_releasedPageScrollStates removeAllObjects];
2007
2196
  [self detachSharedHeaderPressCancellationGesture];
@@ -2027,6 +2216,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2027
2216
  }
2028
2217
  NSNumber *alwaysBounce = [_originalAlwaysBounceVertical objectForKey:scrollView];
2029
2218
  if (alwaysBounce != nil) scrollView.alwaysBounceVertical = alwaysBounce.boolValue;
2219
+ NSNumber *directionalLock = [_originalDirectionalLockEnabled objectForKey:scrollView];
2220
+ if (directionalLock != nil) scrollView.directionalLockEnabled = directionalLock.boolValue;
2030
2221
  NSNumber *adjustment = [_originalInsetAdjustmentBehavior objectForKey:scrollView];
2031
2222
  if (adjustment != nil) {
2032
2223
  scrollView.contentInsetAdjustmentBehavior = (UIScrollViewContentInsetAdjustmentBehavior)adjustment.integerValue;
@@ -2036,6 +2227,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2036
2227
  [_appliedTopInsets removeAllObjects];
2037
2228
  [_originalIndicatorInsets removeAllObjects];
2038
2229
  [_originalAlwaysBounceVertical removeAllObjects];
2230
+ [_originalDirectionalLockEnabled removeAllObjects];
2039
2231
  [_originalInsetAdjustmentBehavior removeAllObjects];
2040
2232
  // OneKey patch: Fabric reuses these views without resetting transforms
2041
2233
  // written by a native parent. Release our collapse translation with the slot.
@@ -2280,13 +2472,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2280
2472
  currentVerticalScrollViewForParentPager];
2281
2473
  }
2282
2474
  if ([view isKindOfClass:RNCNativeScrollerComponentView.class]) {
2283
- return ((RNCNativeScrollerComponentView *)view).scrollView;
2475
+ UIScrollView *scrollView = ((RNCNativeScrollerComponentView *)view).scrollView;
2476
+ return [_mountingRemovedScrollViews containsObject:scrollView] ? nil : scrollView;
2284
2477
  }
2285
2478
  if ([view isKindOfClass:RCTScrollViewComponentView.class]) {
2286
2479
  RCTScrollViewComponentView *componentView =
2287
2480
  (RCTScrollViewComponentView *)view;
2288
2481
  if ([componentView.nativeId hasPrefix:RNCCollapsiblePagerNativeScrollerIDPrefix]) {
2289
- return componentView.scrollView;
2482
+ UIScrollView *scrollView = componentView.scrollView;
2483
+ return [_mountingRemovedScrollViews containsObject:scrollView] ? nil : scrollView;
2290
2484
  }
2291
2485
  }
2292
2486
  for (UIView *subview in view.subviews) {
@@ -2307,7 +2501,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2307
2501
  if ([view isKindOfClass:RCTScrollViewComponentView.class]) {
2308
2502
  UIScrollView *scrollView =
2309
2503
  ((RCTScrollViewComponentView *)view).scrollView;
2310
- if (!scrollView.alwaysBounceHorizontal) {
2504
+ if (!scrollView.alwaysBounceHorizontal &&
2505
+ ![_mountingRemovedScrollViews containsObject:scrollView]) {
2311
2506
  return scrollView;
2312
2507
  }
2313
2508
  }
@@ -2320,7 +2515,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2320
2515
 
2321
2516
  - (UIScrollView *)findVerticalScrollViewInView:(UIView *)view
2322
2517
  {
2323
- if (view == _sharedHeaderHostView) return nil;
2518
+ if (view == _sharedHeaderHostView ||
2519
+ ([view isKindOfClass:UIScrollView.class] &&
2520
+ [_mountingRemovedScrollViews containsObject:(UIScrollView *)view])) return nil;
2324
2521
  // A parent pager must follow the nested pager's active page instead of the
2325
2522
  // first retained page found by recursive subview order.
2326
2523
  if (view != self &&
@@ -2332,6 +2529,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2332
2529
  }
2333
2530
  if ([view isKindOfClass:UICollectionView.class]) {
2334
2531
  UICollectionView *collectionView = (UICollectionView *)view;
2532
+ if (RNCNativeListComponentForScrollView(collectionView) != nil &&
2533
+ !RNCIsMountedNativeListScrollView(collectionView)) return nil;
2335
2534
  UICollectionViewLayout *layout = collectionView.collectionViewLayout;
2336
2535
  BOOL hasUsableBounds = CGRectGetWidth(collectionView.bounds) > 0 &&
2337
2536
  CGRectGetHeight(collectionView.bounds) > 0;
@@ -2407,6 +2606,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2407
2606
  _pendingFallbackScrollViewDetectedAt = 0;
2408
2607
  }
2409
2608
  UIScrollView *candidate = [self verticalScrollViewForPageAtIndex:_currentIndex];
2609
+ if (_nativeSmoothHeaderScrollEnabled && _currentIndex >= 0 && _currentIndex < _pageControllers.count) {
2610
+ RNCGuardNestedHorizontalPans(_pageControllers[_currentIndex].view, _verticalPagerGesture);
2611
+ }
2410
2612
  if (candidate == nil) {
2411
2613
  [self detachScrollObserver];
2412
2614
  [self restoreSharedHeadersToContainer];
@@ -2425,7 +2627,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2425
2627
  return;
2426
2628
  }
2427
2629
  if ([candidate isKindOfClass:UICollectionView.class] &&
2428
- candidate != _observedScrollView) {
2630
+ candidate != _observedScrollView && !RNCIsMountedNativeListScrollView(candidate)) {
2429
2631
  if (_pendingFallbackScrollView != candidate) {
2430
2632
  _pendingFallbackScrollView = candidate;
2431
2633
  _pendingFallbackScrollViewDetectedAt = CACurrentMediaTime();
@@ -2531,8 +2733,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2531
2733
  - (void)connectVerticalPagerGuard
2532
2734
  {
2533
2735
  if (_pagerScrollView != nil && _verticalPagerGesture != nil) {
2534
- [_pagerScrollView.panGestureRecognizer
2535
- requireGestureRecognizerToFail:_verticalPagerGesture];
2736
+ _verticalPagerGesture.pagerPan = _pagerScrollView.panGestureRecognizer;
2737
+ for (UIGestureRecognizer *gesture in _pagerScrollView.gestureRecognizers) {
2738
+ [gesture requireGestureRecognizerToFail:_verticalPagerGesture];
2739
+ }
2536
2740
  }
2537
2741
  }
2538
2742
 
@@ -2787,6 +2991,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2787
2991
  [_originalIndicatorInsets objectForKey:scrollView].UIEdgeInsetsValue;
2788
2992
  scrollView.alwaysBounceVertical =
2789
2993
  [_originalAlwaysBounceVertical objectForKey:scrollView].boolValue;
2994
+ scrollView.directionalLockEnabled =
2995
+ [_originalDirectionalLockEnabled objectForKey:scrollView].boolValue;
2790
2996
  scrollView.contentInsetAdjustmentBehavior = (UIScrollViewContentInsetAdjustmentBehavior)
2791
2997
  [_originalInsetAdjustmentBehavior objectForKey:scrollView].integerValue;
2792
2998
  [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, logicalOffset - original.top)
@@ -2795,6 +3001,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2795
3001
  [_appliedTopInsets removeObjectForKey:scrollView];
2796
3002
  [_originalIndicatorInsets removeObjectForKey:scrollView];
2797
3003
  [_originalAlwaysBounceVertical removeObjectForKey:scrollView];
3004
+ [_originalDirectionalLockEnabled removeObjectForKey:scrollView];
2798
3005
  [_originalInsetAdjustmentBehavior removeObjectForKey:scrollView];
2799
3006
  }
2800
3007
 
@@ -2831,7 +3038,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2831
3038
  // debounce each replacement candidate independently before rejecting the
2832
3039
  // released identity. A surviving native list is safe to accept immediately.
2833
3040
  BOOL needsFallbackDebounce = !sameList &&
2834
- [scrollView isKindOfClass:UICollectionView.class] && scrollView != _observedScrollView;
3041
+ [scrollView isKindOfClass:UICollectionView.class] && scrollView != _observedScrollView &&
3042
+ !RNCIsMountedNativeListScrollView(scrollView);
2835
3043
  if (needsFallbackDebounce) {
2836
3044
  CFTimeInterval now = CACurrentMediaTime();
2837
3045
  if (released.pendingFallbackScrollView != scrollView) {
@@ -2879,12 +3087,17 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
2879
3087
  setObject:[NSValue valueWithUIEdgeInsets:scrollView.verticalScrollIndicatorInsets]
2880
3088
  forKey:scrollView];
2881
3089
  [_originalAlwaysBounceVertical setObject:@(scrollView.alwaysBounceVertical) forKey:scrollView];
3090
+ [_originalDirectionalLockEnabled setObject:@(scrollView.directionalLockEnabled) forKey:scrollView];
2882
3091
  [_originalInsetAdjustmentBehavior setObject:@(scrollView.contentInsetAdjustmentBehavior) forKey:scrollView];
2883
3092
  }
2884
3093
 
2885
3094
  // This host already includes the fixed header and safe area in its frame.
2886
3095
  // UIKit automatic adjustment would add them a second time to short pages.
2887
3096
  scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
3097
+ // This viewport is vertical-only; UIKit's first horizontal lock must not
3098
+ // suppress its later vertical movement while the pager is still pending.
3099
+ scrollView.directionalLockEnabled = _nativeSmoothHeaderScrollEnabled
3100
+ ? NO : [_originalDirectionalLockEnabled objectForKey:scrollView].boolValue;
2888
3101
  [self updateNativeScrollerViewport:scrollView stickyHeight:_stickyHeaderHeight];
2889
3102
  [self updateRefreshControlForScrollView:scrollView];
2890
3103
  CGFloat previousTopInset = scrollView.contentInset.top;
@@ -3543,6 +3756,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
3543
3756
  : velocity;
3544
3757
  BOOL hasMotion = MAX(fabs(intent.x), fabs(intent.y)) >= 1;
3545
3758
  BOOL vertical = hasMotion && fabs(intent.y) > fabs(intent.x);
3759
+ if (gestureRecognizer == _sharedHeaderPressCancellationGesture) vertical = hasMotion;
3546
3760
  NSString *role = gestureRecognizer == _sharedHeaderPressCancellationGesture
3547
3761
  ? @"vertical-cancel"
3548
3762
  : @"vertical-pager-guard";
@@ -0,0 +1,150 @@
1
+ #import <XCTest/XCTest.h>
2
+ #import "RNCCollapsiblePagerViewComponentView.h"
3
+ #import "RNCNativeScrollerComponentView.h"
4
+ #import <react/renderer/mounting/MountingTransaction.h>
5
+
6
+ using namespace facebook::react;
7
+
8
+ @interface RNCCollapsiblePagerViewComponentView (FirstFrameTesting)
9
+ - (void)mountingTransactionDidMount:(const MountingTransaction &)transaction
10
+ withSurfaceTelemetry:(const SurfaceTelemetry &)telemetry;
11
+ - (UIScrollView *)findExplicitNativeScrollerInView:(UIView *)view;
12
+ - (UIScrollView *)findVerticalScrollViewInView:(UIView *)view;
13
+ - (void)attachScrollObserverForCurrentPage;
14
+ @end
15
+
16
+ @interface RNCCollapsiblePagerFirstFrameTests : XCTestCase
17
+ @end
18
+
19
+ @implementation RNCCollapsiblePagerFirstFrameTests
20
+
21
+ - (RNCCollapsiblePagerViewComponentView *)pagerWithPageCount:(NSInteger)count
22
+ {
23
+ RNCCollapsiblePagerViewComponentView *pager =
24
+ [[RNCCollapsiblePagerViewComponentView alloc] initWithFrame:CGRectMake(0, 0, 320, 844)];
25
+ [pager setValue:@YES forKey:@"nativeSmoothHeaderScrollEnabled"];
26
+ [pager setValue:@120 forKey:@"headerHeight"];
27
+ [pager setValue:@44 forKey:@"stickyHeaderHeight"];
28
+ UIWindow *window = [[UIWindow alloc] initWithFrame:pager.frame];
29
+ UIViewController *root = [UIViewController new];
30
+ window.rootViewController = root;
31
+ [root.view addSubview:pager];
32
+ window.hidden = NO;
33
+ [self addTeardownBlock:^{
34
+ [pager prepareForRecycle];
35
+ window.hidden = YES;
36
+ }];
37
+ NSMutableArray *pages = [pager valueForKey:@"pageControllers"];
38
+ NSMutableArray<NSString *> *keys = [NSMutableArray new];
39
+ for (NSInteger index = 0; index < count; index++) {
40
+ UIViewController *page = [UIViewController new];
41
+ page.view = [[UIView alloc] initWithFrame:CGRectMake(index * 320, 0, 320, 600)];
42
+ [pager addSubview:page.view];
43
+ [pages addObject:page];
44
+ [keys addObject:[NSString stringWithFormat:@"page-%ld", (long)index]];
45
+ }
46
+ [pager setValue:keys forKey:@"pageKeys"];
47
+ return pager;
48
+ }
49
+
50
+ - (RNCNativeScrollerComponentView *)addScrollerToPager:(RNCCollapsiblePagerViewComponentView *)pager
51
+ atIndex:(NSInteger)index
52
+ {
53
+ RNCNativeScrollerComponentView *scroller = [RNCNativeScrollerComponentView new];
54
+ scroller.frame = CGRectMake(0, 0, 320, 600);
55
+ scroller.scrollView.frame = scroller.bounds;
56
+ scroller.scrollView.contentSize = CGSizeMake(320, 1500);
57
+ UIViewController *page = [pager valueForKey:@"pageControllers"][index];
58
+ [page.view addSubview:scroller];
59
+ return scroller;
60
+ }
61
+
62
+ - (void)finishMount:(RNCCollapsiblePagerViewComponentView *)pager
63
+ {
64
+ MountingTransaction transaction(1, 1, {}, {});
65
+ SurfaceTelemetry telemetry;
66
+ [pager mountingTransactionDidMount:transaction withSurfaceTelemetry:telemetry];
67
+ }
68
+
69
+ - (void)testInteractiveTargetGetsInsetsBeforeSelectionWithoutTakingObserver
70
+ {
71
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:2];
72
+ RNCNativeScrollerComponentView *target = [self addScrollerToPager:pager atIndex:1];
73
+ [pager setValue:@YES forKey:@"transitioning"];
74
+ [pager setValue:@YES forKey:@"isPagerDragging"];
75
+ UIView *header = [pager valueForKey:@"sharedHeaderHostView"];
76
+ UIView *headerOwner = header.superview;
77
+ [self finishMount:pager];
78
+ XCTAssertEqualWithAccuracy(target.scrollView.contentInset.top, 164, 0.01);
79
+ XCTAssertEqualWithAccuracy(target.scrollView.contentOffset.y, -164, 0.01);
80
+ XCTAssertEqualObjects([pager valueForKey:@"currentIndex"], @0);
81
+ XCTAssertNil([pager valueForKey:@"observedScrollView"]);
82
+ XCTAssertEqual(header.superview, headerOwner);
83
+ }
84
+
85
+ - (void)testRepeatedMountDoesNotRestoreAnAlreadyPreparedTarget
86
+ {
87
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:2];
88
+ RNCNativeScrollerComponentView *target = [self addScrollerToPager:pager atIndex:1];
89
+ [pager setValue:@YES forKey:@"transitioning"];
90
+ [self finishMount:pager];
91
+ target.scrollView.contentOffset = CGPointMake(0, 230);
92
+ [self finishMount:pager];
93
+ XCTAssertEqualWithAccuracy(target.scrollView.contentInset.top, 164, 0.01);
94
+ XCTAssertEqualWithAccuracy(target.scrollView.contentOffset.y, 230, 0.01);
95
+ }
96
+
97
+ - (void)testProgrammaticDestinationOutsideAdjacentRangeIsPrepared
98
+ {
99
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:4];
100
+ RNCNativeScrollerComponentView *unrelated = [self addScrollerToPager:pager atIndex:2];
101
+ RNCNativeScrollerComponentView *target = [self addScrollerToPager:pager atIndex:3];
102
+ [pager setValue:@3 forKey:@"destinationIndex"];
103
+ [pager setValue:@YES forKey:@"transitioning"];
104
+ [self finishMount:pager];
105
+ XCTAssertEqualWithAccuracy(target.scrollView.contentInset.top, 164, 0.01);
106
+ XCTAssertEqualWithAccuracy(target.scrollView.contentOffset.y, -164, 0.01);
107
+ XCTAssertEqualWithAccuracy(unrelated.scrollView.contentInset.top, 0, 0.01);
108
+ XCTAssertEqualObjects([pager valueForKey:@"currentIndex"], @0);
109
+ }
110
+
111
+ - (void)testRemovedScrollerIsExcludedUntilDidMount
112
+ {
113
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:2];
114
+ RNCNativeScrollerComponentView *target = [self addScrollerToPager:pager atIndex:1];
115
+ NSHashTable *removed = [pager valueForKey:@"mountingRemovedScrollViews"];
116
+ [removed addObject:target.scrollView];
117
+ XCTAssertNil([pager findExplicitNativeScrollerInView:target]);
118
+ XCTAssertNil([pager findVerticalScrollViewInView:target.scrollView]);
119
+ [pager setValue:@YES forKey:@"transitioning"];
120
+ [self finishMount:pager];
121
+ XCTAssertEqual(removed.count, 0u);
122
+ XCTAssertEqual([pager findExplicitNativeScrollerInView:target], target.scrollView);
123
+ XCTAssertEqualWithAccuracy(target.scrollView.contentInset.top, 164, 0.01);
124
+ }
125
+
126
+ - (void)testGenericCollectionStillRequiresFallbackDebounce
127
+ {
128
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
129
+ UICollectionView *collection = [[UICollectionView alloc]
130
+ initWithFrame:CGRectMake(0, 0, 320, 600)
131
+ collectionViewLayout:[UICollectionViewFlowLayout new]];
132
+ UIViewController *page = [pager valueForKey:@"pageControllers"][0];
133
+ [page.view addSubview:collection];
134
+ [pager attachScrollObserverForCurrentPage];
135
+ XCTAssertNil([pager valueForKey:@"observedScrollView"]);
136
+ XCTAssertEqualObjects([pager valueForKey:@"pendingFallbackScrollView"], collection);
137
+ XCTAssertEqualWithAccuracy(collection.contentInset.top, 0, 0.01);
138
+ }
139
+
140
+ - (void)testRecycleClearsTransactionExclusion
141
+ {
142
+ RNCCollapsiblePagerViewComponentView *pager = [self pagerWithPageCount:1];
143
+ RNCNativeScrollerComponentView *scroller = [self addScrollerToPager:pager atIndex:0];
144
+ NSHashTable *removed = [pager valueForKey:@"mountingRemovedScrollViews"];
145
+ [removed addObject:scroller.scrollView];
146
+ [pager prepareForRecycle];
147
+ XCTAssertEqual(removed.count, 0u);
148
+ }
149
+
150
+ @end
@@ -165,7 +165,9 @@ typedef struct __IOHIDEvent *RNCTestHIDEventRef;
165
165
  [_window sendEvent:_event];
166
166
  if (phase == UITouchPhaseEnded || phase == UITouchPhaseCancelled) {
167
167
  [_activeTouches removeObjectIdenticalTo:changed];
168
- if (_activeTouches.count == 0) [_event _clearTouches];
168
+ // A finished finger must not be delivered again when another finger ends.
169
+ [_event _clearTouches];
170
+ for (UITouch *touch in _activeTouches) [_event _addTouch:touch forDelayedDelivery:NO];
169
171
  }
170
172
  }
171
173
 
@@ -621,6 +623,7 @@ typedef struct __IOHIDEvent *RNCTestHIDEventRef;
621
623
  @property (nonatomic, strong) RNCCollapsiblePagerViewComponentView *pager;
622
624
  @property (nonatomic, strong) RNCSynthesizedTouchDriver *driver;
623
625
  @property (nonatomic, strong) UIView *firstPage;
626
+ @property (nonatomic) NSUInteger longPressBegins;
624
627
  @end
625
628
 
626
629
  @implementation RNCCollapsiblePagerPressCancellationDeliveryTests
@@ -809,4 +812,111 @@ typedef struct __IOHIDEvent *RNCTestHIDEventRef;
809
812
  XCTAssertEqualObjects(self.handler.events, (@[@"began", @"moved", @"ended"]));
810
813
  }
811
814
 
815
+ - (UIScrollView *)mountNestedHorizontalScrollView
816
+ {
817
+ UIScrollView *vertical = [[UIScrollView alloc] initWithFrame:self.firstPage.bounds];
818
+ vertical.contentSize = CGSizeMake(CGRectGetWidth(vertical.bounds), 1800);
819
+ [self.firstPage addSubview:vertical];
820
+ UIScrollView *horizontal = [[UIScrollView alloc] initWithFrame:CGRectMake(40, 350, 300, 120)];
821
+ horizontal.contentSize = CGSizeMake(900, 120);
822
+ horizontal.contentOffset = CGPointMake(200, 0);
823
+ [vertical addSubview:horizontal];
824
+ UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc]
825
+ initWithTarget:self action:@selector(nestedLongPress:)];
826
+ longPress.minimumPressDuration = 0.1;
827
+ [horizontal addGestureRecognizer:longPress];
828
+ // Re-run the production attachment path after the descendant mounts.
829
+ ((void (*)(id, SEL))objc_msgSend)(self.pager, NSSelectorFromString(@"attachScrollObserverForCurrentPage"));
830
+ [self.window layoutIfNeeded];
831
+ [self drainMainQueue];
832
+ XCTAssertEqual([self.pager valueForKey:@"observedScrollView"], vertical);
833
+ return horizontal;
834
+ }
835
+
836
+ - (void)nestedLongPress:(UILongPressGestureRecognizer *)recognizer
837
+ {
838
+ if (recognizer.state == UIGestureRecognizerStateBegan) self.longPressBegins += 1;
839
+ }
840
+
841
+ - (CGPoint)pressPointInNestedScrollView:(UIScrollView *)scroll
842
+ {
843
+ CGPoint point = [scroll convertPoint:CGPointMake(scroll.contentOffset.x + 150, 60) toView:self.window];
844
+ XCTAssertEqual([self.window hitTest:point withEvent:nil], scroll);
845
+ return point;
846
+ }
847
+
848
+ - (void)testNestedStationaryNativeLongPressRecognizesBeforeFingerUp
849
+ {
850
+ UIScrollView *scroll = [self mountNestedHorizontalScrollView];
851
+ UITouch *touch = [self.driver beginAt:[self pressPointInNestedScrollView:scroll]];
852
+ UIGestureRecognizer *guard = [self.pager valueForKey:@"verticalPagerGesture"];
853
+ XCTAssertEqual(guard.state, UIGestureRecognizerStatePossible);
854
+ XCTAssertEqual(scroll.panGestureRecognizer.minimumNumberOfTouches, 2);
855
+ XCTestExpectation *hold = [self expectationWithDescription:@"stationary finger held"];
856
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)),
857
+ dispatch_get_main_queue(), ^{ [hold fulfill]; });
858
+ [self waitForExpectations:@[hold] timeout:2];
859
+ // The real UIKit target action must arrive while the finger is still down.
860
+ XCTAssertEqual(self.longPressBegins, 1);
861
+ XCTAssertEqual(touch.phase, UITouchPhaseBegan);
862
+ XCTAssertEqual(guard.state, UIGestureRecognizerStatePossible);
863
+ [self.driver end:touch];
864
+ XCTAssertEqual(scroll.panGestureRecognizer.minimumNumberOfTouches, 1);
865
+ }
866
+
867
+ - (void)testNestedHorizontalPanConsumesConfirmedHorizontalDrag
868
+ {
869
+ UIScrollView *scroll = [self mountNestedHorizontalScrollView];
870
+ CGPoint point = [self pressPointInNestedScrollView:scroll];
871
+ CGFloat originalOffset = scroll.contentOffset.x;
872
+ UITouch *touch = [self.driver beginAt:point];
873
+ for (NSInteger step = 1; step <= 10; ++step) {
874
+ [self.driver move:touch to:CGPointMake(point.x - 12 * step, point.y)];
875
+ }
876
+ XCTAssertTrue(scroll.isDragging);
877
+ XCTAssertGreaterThan(scroll.contentOffset.x, originalOffset);
878
+ UIScrollView *pager = [self.pager valueForKey:@"pagerScrollView"];
879
+ XCTAssertFalse(pager.isDragging);
880
+ XCTAssertEqual(self.longPressBegins, 0);
881
+ [self.driver end:touch];
882
+ XCTAssertEqual(scroll.panGestureRecognizer.minimumNumberOfTouches, 1);
883
+ }
884
+
885
+ - (void)testNestedPendingHorizontalStartCanBecomeVertical
886
+ {
887
+ UIScrollView *scroll = [self mountNestedHorizontalScrollView];
888
+ CGPoint point = [self pressPointInNestedScrollView:scroll];
889
+ UITouch *touch = [self.driver beginAt:point];
890
+ [self.driver move:touch to:CGPointMake(point.x - 8, point.y)];
891
+ UIGestureRecognizer *guard = [self.pager valueForKey:@"verticalPagerGesture"];
892
+ XCTAssertEqual(guard.state, UIGestureRecognizerStatePossible);
893
+ for (NSInteger step = 1; step <= 8; ++step) {
894
+ [self.driver move:touch to:CGPointMake(point.x - 8, point.y - 12 * step)];
895
+ }
896
+ UIScrollView *vertical = [self.pager valueForKey:@"observedScrollView"];
897
+ UIScrollView *pager = [self.pager valueForKey:@"pagerScrollView"];
898
+ XCTAssertTrue(vertical.isDragging);
899
+ XCTAssertFalse(scroll.isDragging);
900
+ XCTAssertFalse(pager.isDragging);
901
+ XCTAssertEqual(self.longPressBegins, 0);
902
+ [self.driver end:touch];
903
+ XCTAssertEqual(scroll.panGestureRecognizer.minimumNumberOfTouches, 1);
904
+ XCTAssertEqual(pager.panGestureRecognizer.minimumNumberOfTouches, 1);
905
+ }
906
+
907
+ - (void)testNestedHeldPanTouchCountRestoresOnSecondTouch
908
+ {
909
+ UIScrollView *scroll = [self mountNestedHorizontalScrollView];
910
+ CGPoint point = [self pressPointInNestedScrollView:scroll];
911
+ UITouch *first = [self.driver beginAt:point];
912
+ XCTAssertEqual(scroll.panGestureRecognizer.minimumNumberOfTouches, 2);
913
+ UITouch *second = [self.driver beginAt:CGPointMake(point.x + 20, point.y)];
914
+ UIScrollView *pager = [self.pager valueForKey:@"pagerScrollView"];
915
+ XCTAssertEqual(scroll.panGestureRecognizer.minimumNumberOfTouches, 1);
916
+ XCTAssertEqual(pager.panGestureRecognizer.minimumNumberOfTouches, 1);
917
+ [self.driver end:second];
918
+ [self.driver end:first];
919
+ XCTAssertEqual(scroll.panGestureRecognizer.minimumNumberOfTouches, 1);
920
+ }
921
+
812
922
  @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-pager-view",
3
- "version": "3.0.165",
3
+ "version": "3.0.167",
4
4
  "description": "React Native wrapper for Android and iOS ViewPager",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/module/index.js",
@@ -72,7 +72,7 @@
72
72
  "typescript": "^5.9.2"
73
73
  },
74
74
  "peerDependencies": {
75
- "@onekeyfe/react-native-native-logger": "3.0.165",
75
+ "@onekeyfe/react-native-native-logger": "3.0.167",
76
76
  "react": "*",
77
77
  "react-native": "*"
78
78
  },