@onekeyfe/react-native-pager-view 3.0.163 → 3.0.166
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/reactnativepagerview/CollapsiblePagerHost.kt +8 -2
- package/android/src/main/java/com/reactnativepagerview/NativeScrollerView.kt +139 -6
- package/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt +75 -0
- package/docs/SPEC.md +115 -0
- package/ios/RNCCollapsiblePagerViewComponentView.mm +232 -16
- package/ios/tests/RNCCollapsiblePagerFirstFrameTests.mm +150 -0
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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)
|
|
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 &&
|
|
572
|
-
|
|
573
|
-
|
|
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,69 @@ 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
|
+
second touch in the pending Pager guard fails recognition; an already active
|
|
251
|
+
Pager guard cancels and restores each held pan's original minimum touch count.
|
|
252
|
+
|
|
190
253
|
## Native tab-press animation default
|
|
191
254
|
|
|
192
255
|
Behavior change: previously an omitted `nativeTabPressAnimationEnabled` animated
|
|
@@ -523,3 +586,55 @@ introduced. This change makes no frame-rate claim.
|
|
|
523
586
|
The newly merged NativeScroller viewport tracking uses the same weak-key
|
|
524
587
|
snapshot rule. Its cleanup has source and focused regression coverage; the
|
|
525
588
|
pre-migration runtime checks above do not establish its device acceptance.
|
|
589
|
+
|
|
590
|
+
## iOS lazy-page first-frame preparation
|
|
591
|
+
|
|
592
|
+
Status: implemented and runtime verified in the consuming app's local native
|
|
593
|
+
build on iOS 26.5 (2026-10-10). This package change preserves the same functional
|
|
594
|
+
source; app-specific diagnostic logs remain in that local build.
|
|
595
|
+
|
|
596
|
+
In smooth-header mode, a newly mounted, owned NativeScroller or live NativeList
|
|
597
|
+
on the current page, either adjacent page, or a programmatic destination MUST
|
|
598
|
+
receive its pager insets and restored offset in Fabric's did-mount callback,
|
|
599
|
+
before the next frame. This also applies during a horizontal drag or animation.
|
|
600
|
+
Preparation MUST NOT change the selected page, move the shared header out of its
|
|
601
|
+
transition owner, attach an offscreen page's observer, or repeatedly restore an
|
|
602
|
+
already managed scroll view.
|
|
603
|
+
|
|
604
|
+
A scroll view removed in a Fabric transaction MUST be excluded from discovery
|
|
605
|
+
until that transaction finishes. The exclusion uses weak native identities,
|
|
606
|
+
is cleared after did-mount and on recycle, and allows a moved view to be found
|
|
607
|
+
again in its next owner. A NativeList collection is usable only while its nearest
|
|
608
|
+
Fabric component owns it through contentView, has a registered tag, and retains
|
|
609
|
+
that content view as collection delegate. A disposed or unregistered NativeList
|
|
610
|
+
MUST NOT become a generic collection fallback.
|
|
611
|
+
|
|
612
|
+
Verified NativeList collections bypass the existing 0.5-second generic collection
|
|
613
|
+
fallback debounce, including replacement-state restoration. Other collection
|
|
614
|
+
fallbacks keep that safety delay. Caller insets and retained logical offsets
|
|
615
|
+
remain governed by the existing restoration policy.
|
|
616
|
+
|
|
617
|
+
This is a UI-thread, main-runtime native coordination change with no new props,
|
|
618
|
+
events, background-runtime work, persistence or retained strong view cache.
|
|
619
|
+
Android already prepares discovered scroll children before draw and is unchanged;
|
|
620
|
+
Web is unchanged. Consumers own the decision to lazily mount or prepare a target
|
|
621
|
+
page; this package does not import scene code or start business requests.
|
|
622
|
+
|
|
623
|
+
The Home consumer uses the same lazy-mount trigger on iOS and Android: after
|
|
624
|
+
entering the horizontal dragging state, the first received page-scroll progress
|
|
625
|
+
with nonzero displacement relative to the starting page MUST trigger early
|
|
626
|
+
mounting of the adjacent target page in that direction. There is no fixed
|
|
627
|
+
distance or percentage threshold. Measured trigger percentages, such as 0.65%,
|
|
628
|
+
are event samples rather than configured thresholds. Early mounting MUST NOT
|
|
629
|
+
change the selected page; selection follows the existing native selection event.
|
|
630
|
+
|
|
631
|
+
Conformance: RNCCollapsiblePagerViewComponentView.mm and the focused ownership /
|
|
632
|
+
first-frame XCTest cases. Consumer acceptance confirmed a cold Spot-to-Perps
|
|
633
|
+
swipe: the native skeleton received inset 340 and offset -340 at t=1791603349124
|
|
634
|
+
while transition=1, 427 ms before native drag settlement. Its window y=514 was
|
|
635
|
+
below the header bottom y=502. These values are measured evidence, not package
|
|
636
|
+
constants. Native view geometry is confirmed. Later finite consumer round3 simulator
|
|
637
|
+
cases additionally verified recorded refresh, header, restoration and gesture
|
|
638
|
+
paths; they do not establish every possible sequence or runtime acceptance of a
|
|
639
|
+
newly installed module release. Physical-device feel and semantic accessibility
|
|
640
|
+
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,26 @@ 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
|
+
for (UIGestureRecognizer *gesture in scroll.gestureRecognizers) {
|
|
203
|
+
[gesture requireGestureRecognizerToFail:guard];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (UIView *child in view.subviews) RNCGuardNestedHorizontalPans(child, guard);
|
|
208
|
+
}
|
|
209
|
+
|
|
168
210
|
static void RNCCollapsiblePagerLog(NSString *message)
|
|
169
211
|
{
|
|
170
212
|
Class logClass = NSClassFromString(@"ReactNativeNativeLogger.OneKeyLog");
|
|
@@ -995,6 +1037,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
995
1037
|
intent.y]);
|
|
996
1038
|
return NO;
|
|
997
1039
|
}
|
|
1040
|
+
UIView *target = [self hitTest:[pan locationInView:self] withEvent:nil];
|
|
1041
|
+
for (UIView *ancestor = target; ancestor != nil && ancestor != self; ancestor = ancestor.superview) {
|
|
1042
|
+
if (![ancestor isKindOfClass:UIScrollView.class]) continue;
|
|
1043
|
+
UIScrollView *inner = (UIScrollView *)ancestor;
|
|
1044
|
+
CGFloat maximum = MAX(0, inner.contentSize.width - CGRectGetWidth(inner.bounds));
|
|
1045
|
+
BOOL canScroll = maximum > 0 && (intent.x < 0
|
|
1046
|
+
? inner.contentOffset.x < maximum - 0.5 : inner.contentOffset.x > 0.5);
|
|
1047
|
+
if (canScroll) return NO;
|
|
1048
|
+
}
|
|
998
1049
|
}
|
|
999
1050
|
if (gestureRecognizer == self.panGestureRecognizer && self.excludedHeaderView != nil) {
|
|
1000
1051
|
CGPoint point = [gestureRecognizer locationInView:self.excludedHeaderView];
|
|
@@ -1187,16 +1238,134 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1187
1238
|
|
|
1188
1239
|
@end
|
|
1189
1240
|
|
|
1190
|
-
@interface RNCCollapsiblePagerVerticalPagerGuardGestureRecognizer :
|
|
1241
|
+
@interface RNCCollapsiblePagerVerticalPagerGuardGestureRecognizer : UIGestureRecognizer
|
|
1242
|
+
@property (nonatomic, weak) UIPanGestureRecognizer *pagerPan;
|
|
1243
|
+
- (CGPoint)translationInView:(UIView *)view;
|
|
1244
|
+
- (CGPoint)velocityInView:(UIView *)view;
|
|
1191
1245
|
@end
|
|
1192
1246
|
|
|
1193
|
-
@implementation RNCCollapsiblePagerVerticalPagerGuardGestureRecognizer
|
|
1247
|
+
@implementation RNCCollapsiblePagerVerticalPagerGuardGestureRecognizer {
|
|
1248
|
+
__weak UITouch *_trackedTouch;
|
|
1249
|
+
NSMapTable<UIPanGestureRecognizer *, NSNumber *> *_heldPans;
|
|
1250
|
+
CGPoint _startPoint;
|
|
1251
|
+
CGPoint _lastPoint;
|
|
1252
|
+
CGPoint _velocity;
|
|
1253
|
+
NSTimeInterval _lastTimestamp;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
- (void)holdPan:(UIPanGestureRecognizer *)pan
|
|
1257
|
+
{
|
|
1258
|
+
if (pan == nil || [_heldPans objectForKey:pan] != nil) return;
|
|
1259
|
+
if (_heldPans == nil) _heldPans = [NSMapTable weakToStrongObjectsMapTable];
|
|
1260
|
+
[_heldPans setObject:@(pan.minimumNumberOfTouches) forKey:pan];
|
|
1261
|
+
// A failure dependency alone lets UIKit's pager pan cancel a descendant pan
|
|
1262
|
+
// before intent resolves. Keep DOWN tracked while deferring one-finger begin.
|
|
1263
|
+
pan.minimumNumberOfTouches = MAX(2, pan.minimumNumberOfTouches);
|
|
1264
|
+
}
|
|
1194
1265
|
|
|
1195
|
-
- (
|
|
1266
|
+
- (void)restoreHeldPans
|
|
1196
1267
|
{
|
|
1197
|
-
|
|
1268
|
+
for (UIPanGestureRecognizer *pan in _heldPans.keyEnumerator) {
|
|
1269
|
+
pan.minimumNumberOfTouches = [_heldPans objectForKey:pan].unsignedIntegerValue;
|
|
1270
|
+
UIScrollView *scroll = [pan.view isKindOfClass:UIScrollView.class] ? (UIScrollView *)pan.view : nil;
|
|
1271
|
+
RNCCollapsiblePagerLog([NSString stringWithFormat:
|
|
1272
|
+
@"gesture-restore pan=%@ min-touches=%lu offset=%@",
|
|
1273
|
+
NSStringFromClass(pan.view.class), (unsigned long)pan.minimumNumberOfTouches,
|
|
1274
|
+
scroll != nil ? NSStringFromCGPoint(scroll.contentOffset) : @"none"]);
|
|
1275
|
+
}
|
|
1276
|
+
[_heldPans removeAllObjects];
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
- (CGPoint)translationInView:(UIView *)view
|
|
1280
|
+
{
|
|
1281
|
+
CGPoint start = [view convertPoint:_startPoint fromView:self.view];
|
|
1282
|
+
CGPoint last = [view convertPoint:_lastPoint fromView:self.view];
|
|
1283
|
+
return CGPointMake(last.x - start.x, last.y - start.y);
|
|
1198
1284
|
}
|
|
1199
1285
|
|
|
1286
|
+
- (CGPoint)velocityInView:(UIView *)view { return _velocity; }
|
|
1287
|
+
|
|
1288
|
+
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
|
|
1289
|
+
{
|
|
1290
|
+
if (_trackedTouch != nil || touches.count != 1) {
|
|
1291
|
+
RNCCollapsiblePagerLog([NSString stringWithFormat:
|
|
1292
|
+
@"gesture-second-touch previous-state=%ld incoming=%lu held-pans=%lu",
|
|
1293
|
+
(long)self.state, (unsigned long)touches.count, (unsigned long)_heldPans.count]);
|
|
1294
|
+
[self restoreHeldPans];
|
|
1295
|
+
if (self.state == UIGestureRecognizerStatePossible) {
|
|
1296
|
+
self.state = UIGestureRecognizerStateFailed;
|
|
1297
|
+
} else if (self.state == UIGestureRecognizerStateBegan || self.state == UIGestureRecognizerStateChanged) {
|
|
1298
|
+
self.state = UIGestureRecognizerStateCancelled;
|
|
1299
|
+
}
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
_trackedTouch = touches.anyObject;
|
|
1303
|
+
_startPoint = _lastPoint = [touches.anyObject locationInView:self.view];
|
|
1304
|
+
_lastTimestamp = event.timestamp;
|
|
1305
|
+
_velocity = CGPointZero;
|
|
1306
|
+
[self holdPan:self.pagerPan];
|
|
1307
|
+
for (UIView *view = _trackedTouch.view; view != nil && view != self.view; view = view.superview) {
|
|
1308
|
+
if (![view isKindOfClass:UIScrollView.class]) continue;
|
|
1309
|
+
UIScrollView *scroll = (UIScrollView *)view;
|
|
1310
|
+
if (RNCIsEnabledHorizontalScrollOwner(scroll)) {
|
|
1311
|
+
RNCCollapsiblePagerLog([NSString stringWithFormat:
|
|
1312
|
+
@"gesture-hold inner=%@ paging=%d offset=%@ content=%@ bounds=%@",
|
|
1313
|
+
NSStringFromClass(scroll.class), scroll.pagingEnabled, NSStringFromCGPoint(scroll.contentOffset),
|
|
1314
|
+
NSStringFromCGSize(scroll.contentSize), NSStringFromCGSize(scroll.bounds.size)]);
|
|
1315
|
+
[self holdPan:scroll.panGestureRecognizer];
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
|
|
1321
|
+
{
|
|
1322
|
+
CGPoint point = [touches.anyObject locationInView:self.view];
|
|
1323
|
+
NSTimeInterval elapsed = event.timestamp - _lastTimestamp;
|
|
1324
|
+
if (elapsed > 0) {
|
|
1325
|
+
_velocity = CGPointMake((point.x - _lastPoint.x) / elapsed,
|
|
1326
|
+
(point.y - _lastPoint.y) / elapsed);
|
|
1327
|
+
}
|
|
1328
|
+
_lastPoint = point;
|
|
1329
|
+
_lastTimestamp = event.timestamp;
|
|
1330
|
+
if (self.state == UIGestureRecognizerStatePossible) {
|
|
1331
|
+
CGFloat dx = fabs(point.x - _startPoint.x), dy = fabs(point.y - _startPoint.y);
|
|
1332
|
+
// Keep intent pending until the explicit distance decision, instead of
|
|
1333
|
+
// relying on UIPanGestureRecognizer's earlier recognition.
|
|
1334
|
+
if ((dy >= 10 && dy > dx) || (dx >= 32 && dx > dy)) {
|
|
1335
|
+
RNCCollapsiblePagerLog([NSString stringWithFormat:
|
|
1336
|
+
@"gesture-intent axis=%@ dx=%.2f dy=%.2f held-pans=%lu",
|
|
1337
|
+
dy > dx ? @"vertical" : @"horizontal", dx, dy, (unsigned long)_heldPans.count]);
|
|
1338
|
+
self.state = UIGestureRecognizerStateBegan;
|
|
1339
|
+
if (self.state == UIGestureRecognizerStateFailed) [self restoreHeldPans];
|
|
1340
|
+
}
|
|
1341
|
+
} else if (self.state == UIGestureRecognizerStateBegan || self.state == UIGestureRecognizerStateChanged) {
|
|
1342
|
+
self.state = UIGestureRecognizerStateChanged;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
|
|
1347
|
+
{
|
|
1348
|
+
[self restoreHeldPans];
|
|
1349
|
+
self.state = self.state == UIGestureRecognizerStatePossible
|
|
1350
|
+
? UIGestureRecognizerStateFailed : UIGestureRecognizerStateEnded;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
|
|
1354
|
+
{
|
|
1355
|
+
[self restoreHeldPans];
|
|
1356
|
+
self.state = UIGestureRecognizerStateCancelled;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
- (void)reset
|
|
1360
|
+
{
|
|
1361
|
+
[self restoreHeldPans];
|
|
1362
|
+
[super reset];
|
|
1363
|
+
_trackedTouch = nil;
|
|
1364
|
+
_velocity = CGPointZero;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
- (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)other { return NO; }
|
|
1368
|
+
- (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)other { return YES; }
|
|
1200
1369
|
@end
|
|
1201
1370
|
|
|
1202
1371
|
@interface RNCCollapsiblePagerViewComponentView () <
|
|
@@ -1282,10 +1451,12 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1282
1451
|
NSMapTable<UIScrollView *, NSNumber *> *_appliedTopInsets;
|
|
1283
1452
|
NSMapTable<UIScrollView *, NSValue *> *_originalIndicatorInsets;
|
|
1284
1453
|
NSMapTable<UIScrollView *, NSNumber *> *_originalAlwaysBounceVertical;
|
|
1454
|
+
NSMapTable<UIScrollView *, NSNumber *> *_originalDirectionalLockEnabled;
|
|
1285
1455
|
NSMapTable<UIScrollView *, NSNumber *> *_originalInsetAdjustmentBehavior;
|
|
1286
1456
|
// OneKey patch: pair pre-mount release with recovery, including deferred mounts.
|
|
1287
1457
|
NSMutableDictionary<NSString *, RNCCollapsiblePagerReleasedScrollState *> *_releasedPageScrollStates;
|
|
1288
1458
|
BOOL _needsScrollObserverReattachAfterMount;
|
|
1459
|
+
NSHashTable<UIScrollView *> *_mountingRemovedScrollViews;
|
|
1289
1460
|
__weak UIScrollView *_observedScrollView;
|
|
1290
1461
|
__weak UIScrollView *_sharedHeaderScrollView;
|
|
1291
1462
|
__weak UIScrollView *_pendingFallbackScrollView;
|
|
@@ -1294,7 +1465,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1294
1465
|
UIPanGestureRecognizer *_sharedHeaderPressCancellationGesture;
|
|
1295
1466
|
RNCCollapsiblePagerContentPressCancellationGestureRecognizer *_contentPressCancellationGesture;
|
|
1296
1467
|
RNCCollapsiblePagerOuterPagerPanGestureRecognizer *_sharedHeaderOuterPagerGesture;
|
|
1297
|
-
|
|
1468
|
+
RNCCollapsiblePagerVerticalPagerGuardGestureRecognizer *_verticalPagerGesture;
|
|
1298
1469
|
BOOL _observingContentOffset;
|
|
1299
1470
|
BOOL _scrollResolveRetryScheduled;
|
|
1300
1471
|
CFTimeInterval _pendingFallbackScrollViewDetectedAt;
|
|
@@ -1322,12 +1493,14 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1322
1493
|
_pageControllers = [NSMutableArray new];
|
|
1323
1494
|
_pageOffsets = [NSMutableDictionary new];
|
|
1324
1495
|
_releasedPageScrollStates = [NSMutableDictionary new];
|
|
1496
|
+
_mountingRemovedScrollViews = [NSHashTable weakObjectsHashTable];
|
|
1325
1497
|
_originalInsets = [NSMapTable weakToStrongObjectsMapTable];
|
|
1326
1498
|
_managedRefreshControls = [NSMapTable weakToWeakObjectsMapTable];
|
|
1327
1499
|
_refreshControlObservedScrollViews = [NSHashTable weakObjectsHashTable];
|
|
1328
1500
|
_appliedTopInsets = [NSMapTable weakToStrongObjectsMapTable];
|
|
1329
1501
|
_originalIndicatorInsets = [NSMapTable weakToStrongObjectsMapTable];
|
|
1330
1502
|
_originalAlwaysBounceVertical = [NSMapTable weakToStrongObjectsMapTable];
|
|
1503
|
+
_originalDirectionalLockEnabled = [NSMapTable weakToStrongObjectsMapTable];
|
|
1331
1504
|
_originalInsetAdjustmentBehavior = [NSMapTable weakToStrongObjectsMapTable];
|
|
1332
1505
|
_pageKeys = @[];
|
|
1333
1506
|
_retainedPages = @"[]";
|
|
@@ -1365,7 +1538,6 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1365
1538
|
_verticalPagerGesture.cancelsTouchesInView = NO;
|
|
1366
1539
|
_verticalPagerGesture.enabled = NO;
|
|
1367
1540
|
[self addGestureRecognizer:_verticalPagerGesture];
|
|
1368
|
-
|
|
1369
1541
|
_nativeTabBarView = [RNCCollapsiblePagerNativeTabBarView new];
|
|
1370
1542
|
_nativeTabBarView.hidden = YES;
|
|
1371
1543
|
__weak __typeof__(self) weakSelf = self;
|
|
@@ -1758,6 +1930,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1758
1930
|
}
|
|
1759
1931
|
break;
|
|
1760
1932
|
}
|
|
1933
|
+
// Quarantine only this mount transaction; moved views may be valid again.
|
|
1934
|
+
[_mountingRemovedScrollViews addObject:scrollView];
|
|
1761
1935
|
_needsScrollObserverReattachAfterMount = YES;
|
|
1762
1936
|
[self restoreScrollViewState:scrollView resetSavedOffset:!belongsToPage];
|
|
1763
1937
|
break;
|
|
@@ -1768,9 +1942,25 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
1768
1942
|
- (void)mountingTransactionDidMount:(const MountingTransaction &)transaction
|
|
1769
1943
|
withSurfaceTelemetry:(const SurfaceTelemetry &)surfaceTelemetry
|
|
1770
1944
|
{
|
|
1771
|
-
|
|
1772
|
-
|
|
1945
|
+
[_mountingRemovedScrollViews removeAllObjects];
|
|
1946
|
+
if (!_nativeSmoothHeaderScrollEnabled || _isBeingRecycled || self.window == nil ||
|
|
1773
1947
|
_currentIndex < 0 || _currentIndex >= _pageControllers.count) return;
|
|
1948
|
+
// Prepare newly mounted owned scrollers before their first frame, even while
|
|
1949
|
+
// horizontal selection still owns the shared header and observer.
|
|
1950
|
+
NSInteger first = MAX(0, _currentIndex - 1);
|
|
1951
|
+
NSInteger last = MIN((NSInteger)_pageControllers.count - 1, _currentIndex + 1);
|
|
1952
|
+
for (NSInteger index = 0; index < _pageControllers.count; index++) {
|
|
1953
|
+
if ((index < first || index > last) && index != _destinationIndex) continue;
|
|
1954
|
+
UIScrollView *scrollView = [self verticalScrollViewForPageAtIndex:index];
|
|
1955
|
+
BOOL isOwnedScroller = [scrollView.superview isKindOfClass:RNCNativeScrollerComponentView.class] &&
|
|
1956
|
+
((RNCNativeScrollerComponentView *)scrollView.superview).scrollView == scrollView &&
|
|
1957
|
+
[scrollView isDescendantOfView:_pageControllers[index].view];
|
|
1958
|
+
if ((RNCIsMountedNativeListScrollView(scrollView) || isOwnedScroller) &&
|
|
1959
|
+
[_originalInsets objectForKey:scrollView] == nil) {
|
|
1960
|
+
[self applyInsetsToScrollView:scrollView pageIndex:index restore:YES];
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
if (_transitioning || _isPagerDragging) return;
|
|
1774
1964
|
|
|
1775
1965
|
// OneKey patch: a release must recover even when the changed parent is above
|
|
1776
1966
|
// the page subtree. Missing lists keep their identity for a later insertion.
|
|
@@ -2002,6 +2192,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2002
2192
|
{
|
|
2003
2193
|
_isBeingRecycled = YES;
|
|
2004
2194
|
_generation++;
|
|
2195
|
+
[_mountingRemovedScrollViews removeAllObjects];
|
|
2005
2196
|
_needsScrollObserverReattachAfterMount = NO;
|
|
2006
2197
|
[_releasedPageScrollStates removeAllObjects];
|
|
2007
2198
|
[self detachSharedHeaderPressCancellationGesture];
|
|
@@ -2027,6 +2218,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2027
2218
|
}
|
|
2028
2219
|
NSNumber *alwaysBounce = [_originalAlwaysBounceVertical objectForKey:scrollView];
|
|
2029
2220
|
if (alwaysBounce != nil) scrollView.alwaysBounceVertical = alwaysBounce.boolValue;
|
|
2221
|
+
NSNumber *directionalLock = [_originalDirectionalLockEnabled objectForKey:scrollView];
|
|
2222
|
+
if (directionalLock != nil) scrollView.directionalLockEnabled = directionalLock.boolValue;
|
|
2030
2223
|
NSNumber *adjustment = [_originalInsetAdjustmentBehavior objectForKey:scrollView];
|
|
2031
2224
|
if (adjustment != nil) {
|
|
2032
2225
|
scrollView.contentInsetAdjustmentBehavior = (UIScrollViewContentInsetAdjustmentBehavior)adjustment.integerValue;
|
|
@@ -2036,6 +2229,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2036
2229
|
[_appliedTopInsets removeAllObjects];
|
|
2037
2230
|
[_originalIndicatorInsets removeAllObjects];
|
|
2038
2231
|
[_originalAlwaysBounceVertical removeAllObjects];
|
|
2232
|
+
[_originalDirectionalLockEnabled removeAllObjects];
|
|
2039
2233
|
[_originalInsetAdjustmentBehavior removeAllObjects];
|
|
2040
2234
|
// OneKey patch: Fabric reuses these views without resetting transforms
|
|
2041
2235
|
// written by a native parent. Release our collapse translation with the slot.
|
|
@@ -2280,13 +2474,15 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2280
2474
|
currentVerticalScrollViewForParentPager];
|
|
2281
2475
|
}
|
|
2282
2476
|
if ([view isKindOfClass:RNCNativeScrollerComponentView.class]) {
|
|
2283
|
-
|
|
2477
|
+
UIScrollView *scrollView = ((RNCNativeScrollerComponentView *)view).scrollView;
|
|
2478
|
+
return [_mountingRemovedScrollViews containsObject:scrollView] ? nil : scrollView;
|
|
2284
2479
|
}
|
|
2285
2480
|
if ([view isKindOfClass:RCTScrollViewComponentView.class]) {
|
|
2286
2481
|
RCTScrollViewComponentView *componentView =
|
|
2287
2482
|
(RCTScrollViewComponentView *)view;
|
|
2288
2483
|
if ([componentView.nativeId hasPrefix:RNCCollapsiblePagerNativeScrollerIDPrefix]) {
|
|
2289
|
-
|
|
2484
|
+
UIScrollView *scrollView = componentView.scrollView;
|
|
2485
|
+
return [_mountingRemovedScrollViews containsObject:scrollView] ? nil : scrollView;
|
|
2290
2486
|
}
|
|
2291
2487
|
}
|
|
2292
2488
|
for (UIView *subview in view.subviews) {
|
|
@@ -2307,7 +2503,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2307
2503
|
if ([view isKindOfClass:RCTScrollViewComponentView.class]) {
|
|
2308
2504
|
UIScrollView *scrollView =
|
|
2309
2505
|
((RCTScrollViewComponentView *)view).scrollView;
|
|
2310
|
-
if (!scrollView.alwaysBounceHorizontal
|
|
2506
|
+
if (!scrollView.alwaysBounceHorizontal &&
|
|
2507
|
+
![_mountingRemovedScrollViews containsObject:scrollView]) {
|
|
2311
2508
|
return scrollView;
|
|
2312
2509
|
}
|
|
2313
2510
|
}
|
|
@@ -2320,7 +2517,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2320
2517
|
|
|
2321
2518
|
- (UIScrollView *)findVerticalScrollViewInView:(UIView *)view
|
|
2322
2519
|
{
|
|
2323
|
-
if (view == _sharedHeaderHostView
|
|
2520
|
+
if (view == _sharedHeaderHostView ||
|
|
2521
|
+
([view isKindOfClass:UIScrollView.class] &&
|
|
2522
|
+
[_mountingRemovedScrollViews containsObject:(UIScrollView *)view])) return nil;
|
|
2324
2523
|
// A parent pager must follow the nested pager's active page instead of the
|
|
2325
2524
|
// first retained page found by recursive subview order.
|
|
2326
2525
|
if (view != self &&
|
|
@@ -2332,6 +2531,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2332
2531
|
}
|
|
2333
2532
|
if ([view isKindOfClass:UICollectionView.class]) {
|
|
2334
2533
|
UICollectionView *collectionView = (UICollectionView *)view;
|
|
2534
|
+
if (RNCNativeListComponentForScrollView(collectionView) != nil &&
|
|
2535
|
+
!RNCIsMountedNativeListScrollView(collectionView)) return nil;
|
|
2335
2536
|
UICollectionViewLayout *layout = collectionView.collectionViewLayout;
|
|
2336
2537
|
BOOL hasUsableBounds = CGRectGetWidth(collectionView.bounds) > 0 &&
|
|
2337
2538
|
CGRectGetHeight(collectionView.bounds) > 0;
|
|
@@ -2407,6 +2608,9 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2407
2608
|
_pendingFallbackScrollViewDetectedAt = 0;
|
|
2408
2609
|
}
|
|
2409
2610
|
UIScrollView *candidate = [self verticalScrollViewForPageAtIndex:_currentIndex];
|
|
2611
|
+
if (_nativeSmoothHeaderScrollEnabled && _currentIndex >= 0 && _currentIndex < _pageControllers.count) {
|
|
2612
|
+
RNCGuardNestedHorizontalPans(_pageControllers[_currentIndex].view, _verticalPagerGesture);
|
|
2613
|
+
}
|
|
2410
2614
|
if (candidate == nil) {
|
|
2411
2615
|
[self detachScrollObserver];
|
|
2412
2616
|
[self restoreSharedHeadersToContainer];
|
|
@@ -2425,7 +2629,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2425
2629
|
return;
|
|
2426
2630
|
}
|
|
2427
2631
|
if ([candidate isKindOfClass:UICollectionView.class] &&
|
|
2428
|
-
candidate != _observedScrollView) {
|
|
2632
|
+
candidate != _observedScrollView && !RNCIsMountedNativeListScrollView(candidate)) {
|
|
2429
2633
|
if (_pendingFallbackScrollView != candidate) {
|
|
2430
2634
|
_pendingFallbackScrollView = candidate;
|
|
2431
2635
|
_pendingFallbackScrollViewDetectedAt = CACurrentMediaTime();
|
|
@@ -2531,8 +2735,10 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2531
2735
|
- (void)connectVerticalPagerGuard
|
|
2532
2736
|
{
|
|
2533
2737
|
if (_pagerScrollView != nil && _verticalPagerGesture != nil) {
|
|
2534
|
-
|
|
2535
|
-
|
|
2738
|
+
_verticalPagerGesture.pagerPan = _pagerScrollView.panGestureRecognizer;
|
|
2739
|
+
for (UIGestureRecognizer *gesture in _pagerScrollView.gestureRecognizers) {
|
|
2740
|
+
[gesture requireGestureRecognizerToFail:_verticalPagerGesture];
|
|
2741
|
+
}
|
|
2536
2742
|
}
|
|
2537
2743
|
}
|
|
2538
2744
|
|
|
@@ -2787,6 +2993,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2787
2993
|
[_originalIndicatorInsets objectForKey:scrollView].UIEdgeInsetsValue;
|
|
2788
2994
|
scrollView.alwaysBounceVertical =
|
|
2789
2995
|
[_originalAlwaysBounceVertical objectForKey:scrollView].boolValue;
|
|
2996
|
+
scrollView.directionalLockEnabled =
|
|
2997
|
+
[_originalDirectionalLockEnabled objectForKey:scrollView].boolValue;
|
|
2790
2998
|
scrollView.contentInsetAdjustmentBehavior = (UIScrollViewContentInsetAdjustmentBehavior)
|
|
2791
2999
|
[_originalInsetAdjustmentBehavior objectForKey:scrollView].integerValue;
|
|
2792
3000
|
[scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, logicalOffset - original.top)
|
|
@@ -2795,6 +3003,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2795
3003
|
[_appliedTopInsets removeObjectForKey:scrollView];
|
|
2796
3004
|
[_originalIndicatorInsets removeObjectForKey:scrollView];
|
|
2797
3005
|
[_originalAlwaysBounceVertical removeObjectForKey:scrollView];
|
|
3006
|
+
[_originalDirectionalLockEnabled removeObjectForKey:scrollView];
|
|
2798
3007
|
[_originalInsetAdjustmentBehavior removeObjectForKey:scrollView];
|
|
2799
3008
|
}
|
|
2800
3009
|
|
|
@@ -2831,7 +3040,8 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2831
3040
|
// debounce each replacement candidate independently before rejecting the
|
|
2832
3041
|
// released identity. A surviving native list is safe to accept immediately.
|
|
2833
3042
|
BOOL needsFallbackDebounce = !sameList &&
|
|
2834
|
-
[scrollView isKindOfClass:UICollectionView.class] && scrollView != _observedScrollView
|
|
3043
|
+
[scrollView isKindOfClass:UICollectionView.class] && scrollView != _observedScrollView &&
|
|
3044
|
+
!RNCIsMountedNativeListScrollView(scrollView);
|
|
2835
3045
|
if (needsFallbackDebounce) {
|
|
2836
3046
|
CFTimeInterval now = CACurrentMediaTime();
|
|
2837
3047
|
if (released.pendingFallbackScrollView != scrollView) {
|
|
@@ -2879,12 +3089,17 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
2879
3089
|
setObject:[NSValue valueWithUIEdgeInsets:scrollView.verticalScrollIndicatorInsets]
|
|
2880
3090
|
forKey:scrollView];
|
|
2881
3091
|
[_originalAlwaysBounceVertical setObject:@(scrollView.alwaysBounceVertical) forKey:scrollView];
|
|
3092
|
+
[_originalDirectionalLockEnabled setObject:@(scrollView.directionalLockEnabled) forKey:scrollView];
|
|
2882
3093
|
[_originalInsetAdjustmentBehavior setObject:@(scrollView.contentInsetAdjustmentBehavior) forKey:scrollView];
|
|
2883
3094
|
}
|
|
2884
3095
|
|
|
2885
3096
|
// This host already includes the fixed header and safe area in its frame.
|
|
2886
3097
|
// UIKit automatic adjustment would add them a second time to short pages.
|
|
2887
3098
|
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
|
3099
|
+
// This viewport is vertical-only; UIKit's first horizontal lock must not
|
|
3100
|
+
// suppress its later vertical movement while the pager is still pending.
|
|
3101
|
+
scrollView.directionalLockEnabled = _nativeSmoothHeaderScrollEnabled
|
|
3102
|
+
? NO : [_originalDirectionalLockEnabled objectForKey:scrollView].boolValue;
|
|
2888
3103
|
[self updateNativeScrollerViewport:scrollView stickyHeight:_stickyHeaderHeight];
|
|
2889
3104
|
[self updateRefreshControlForScrollView:scrollView];
|
|
2890
3105
|
CGFloat previousTopInset = scrollView.contentInset.top;
|
|
@@ -3543,6 +3758,7 @@ static void RNCLogNativeTabScrollBoundary(NSString *owner,
|
|
|
3543
3758
|
: velocity;
|
|
3544
3759
|
BOOL hasMotion = MAX(fabs(intent.x), fabs(intent.y)) >= 1;
|
|
3545
3760
|
BOOL vertical = hasMotion && fabs(intent.y) > fabs(intent.x);
|
|
3761
|
+
if (gestureRecognizer == _sharedHeaderPressCancellationGesture) vertical = hasMotion;
|
|
3546
3762
|
NSString *role = gestureRecognizer == _sharedHeaderPressCancellationGesture
|
|
3547
3763
|
? @"vertical-cancel"
|
|
3548
3764
|
: @"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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-pager-view",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.166",
|
|
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.
|
|
75
|
+
"@onekeyfe/react-native-native-logger": "3.0.166",
|
|
76
76
|
"react": "*",
|
|
77
77
|
"react-native": "*"
|
|
78
78
|
},
|