@onekeyfe/react-native-scroll-guard 0.1.1

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.
Files changed (87) hide show
  1. package/ScrollGuard.podspec +29 -0
  2. package/android/CMakeLists.txt +24 -0
  3. package/android/build.gradle +124 -0
  4. package/android/src/main/AndroidManifest.xml +2 -0
  5. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  6. package/android/src/main/java/com/margelo/nitro/scrollguard/ScrollGuard.kt +117 -0
  7. package/android/src/main/java/com/margelo/nitro/scrollguard/ScrollGuardPackage.kt +27 -0
  8. package/android/src/main/java/com/margelo/nitro/scrollguard/ScrollGuardViewGroupManager.kt +48 -0
  9. package/ios/ScrollGuard.swift +243 -0
  10. package/lib/module/ScrollGuard.nitro.js +9 -0
  11. package/lib/module/ScrollGuard.nitro.js.map +1 -0
  12. package/lib/module/index.js +7 -0
  13. package/lib/module/index.js.map +1 -0
  14. package/lib/module/package.json +1 -0
  15. package/lib/nitrogen/generated/android/c++/JHybridScrollGuardSpec.cpp +59 -0
  16. package/lib/nitrogen/generated/android/c++/JHybridScrollGuardSpec.hpp +66 -0
  17. package/lib/nitrogen/generated/android/c++/JScrollGuardDirection.hpp +62 -0
  18. package/lib/nitrogen/generated/android/c++/views/JHybridScrollGuardStateUpdater.cpp +56 -0
  19. package/lib/nitrogen/generated/android/c++/views/JHybridScrollGuardStateUpdater.hpp +49 -0
  20. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/HybridScrollGuardSpec.kt +59 -0
  21. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/ScrollGuardDirection.kt +22 -0
  22. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/scrollguardOnLoad.kt +35 -0
  23. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/views/HybridScrollGuardManager.kt +50 -0
  24. package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/views/HybridScrollGuardStateUpdater.kt +23 -0
  25. package/lib/nitrogen/generated/android/scrollguard+autolinking.cmake +83 -0
  26. package/lib/nitrogen/generated/android/scrollguard+autolinking.gradle +27 -0
  27. package/lib/nitrogen/generated/android/scrollguardOnLoad.cpp +46 -0
  28. package/lib/nitrogen/generated/android/scrollguardOnLoad.hpp +25 -0
  29. package/lib/nitrogen/generated/ios/ScrollGuard+autolinking.rb +60 -0
  30. package/lib/nitrogen/generated/ios/ScrollGuard-Swift-Cxx-Bridge.cpp +33 -0
  31. package/lib/nitrogen/generated/ios/ScrollGuard-Swift-Cxx-Bridge.hpp +59 -0
  32. package/lib/nitrogen/generated/ios/ScrollGuard-Swift-Cxx-Umbrella.hpp +45 -0
  33. package/lib/nitrogen/generated/ios/ScrollGuardAutolinking.mm +33 -0
  34. package/lib/nitrogen/generated/ios/ScrollGuardAutolinking.swift +25 -0
  35. package/lib/nitrogen/generated/ios/c++/HybridScrollGuardSpecSwift.cpp +11 -0
  36. package/lib/nitrogen/generated/ios/c++/HybridScrollGuardSpecSwift.hpp +77 -0
  37. package/lib/nitrogen/generated/ios/c++/views/HybridScrollGuardComponent.mm +96 -0
  38. package/lib/nitrogen/generated/ios/swift/HybridScrollGuardSpec.swift +56 -0
  39. package/lib/nitrogen/generated/ios/swift/HybridScrollGuardSpec_cxx.swift +146 -0
  40. package/lib/nitrogen/generated/ios/swift/ScrollGuardDirection.swift +44 -0
  41. package/lib/nitrogen/generated/shared/c++/HybridScrollGuardSpec.cpp +22 -0
  42. package/lib/nitrogen/generated/shared/c++/HybridScrollGuardSpec.hpp +65 -0
  43. package/lib/nitrogen/generated/shared/c++/ScrollGuardDirection.hpp +80 -0
  44. package/lib/nitrogen/generated/shared/c++/views/HybridScrollGuardComponent.cpp +87 -0
  45. package/lib/nitrogen/generated/shared/c++/views/HybridScrollGuardComponent.hpp +108 -0
  46. package/lib/nitrogen/generated/shared/json/ScrollGuardConfig.json +10 -0
  47. package/lib/typescript/package.json +1 -0
  48. package/lib/typescript/src/ScrollGuard.nitro.d.ts +19 -0
  49. package/lib/typescript/src/ScrollGuard.nitro.d.ts.map +1 -0
  50. package/lib/typescript/src/index.d.ts +5 -0
  51. package/lib/typescript/src/index.d.ts.map +1 -0
  52. package/nitro.json +17 -0
  53. package/nitrogen/generated/android/c++/JHybridScrollGuardSpec.cpp +59 -0
  54. package/nitrogen/generated/android/c++/JHybridScrollGuardSpec.hpp +66 -0
  55. package/nitrogen/generated/android/c++/JScrollGuardDirection.hpp +62 -0
  56. package/nitrogen/generated/android/c++/views/JHybridScrollGuardStateUpdater.cpp +56 -0
  57. package/nitrogen/generated/android/c++/views/JHybridScrollGuardStateUpdater.hpp +49 -0
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/HybridScrollGuardSpec.kt +59 -0
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/ScrollGuardDirection.kt +22 -0
  60. package/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/scrollguardOnLoad.kt +35 -0
  61. package/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/views/HybridScrollGuardManager.kt +50 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/views/HybridScrollGuardStateUpdater.kt +23 -0
  63. package/nitrogen/generated/android/scrollguard+autolinking.cmake +83 -0
  64. package/nitrogen/generated/android/scrollguard+autolinking.gradle +27 -0
  65. package/nitrogen/generated/android/scrollguardOnLoad.cpp +46 -0
  66. package/nitrogen/generated/android/scrollguardOnLoad.hpp +25 -0
  67. package/nitrogen/generated/ios/ScrollGuard+autolinking.rb +60 -0
  68. package/nitrogen/generated/ios/ScrollGuard-Swift-Cxx-Bridge.cpp +33 -0
  69. package/nitrogen/generated/ios/ScrollGuard-Swift-Cxx-Bridge.hpp +59 -0
  70. package/nitrogen/generated/ios/ScrollGuard-Swift-Cxx-Umbrella.hpp +45 -0
  71. package/nitrogen/generated/ios/ScrollGuardAutolinking.mm +33 -0
  72. package/nitrogen/generated/ios/ScrollGuardAutolinking.swift +25 -0
  73. package/nitrogen/generated/ios/c++/HybridScrollGuardSpecSwift.cpp +11 -0
  74. package/nitrogen/generated/ios/c++/HybridScrollGuardSpecSwift.hpp +77 -0
  75. package/nitrogen/generated/ios/c++/views/HybridScrollGuardComponent.mm +96 -0
  76. package/nitrogen/generated/ios/swift/HybridScrollGuardSpec.swift +56 -0
  77. package/nitrogen/generated/ios/swift/HybridScrollGuardSpec_cxx.swift +146 -0
  78. package/nitrogen/generated/ios/swift/ScrollGuardDirection.swift +44 -0
  79. package/nitrogen/generated/shared/c++/HybridScrollGuardSpec.cpp +22 -0
  80. package/nitrogen/generated/shared/c++/HybridScrollGuardSpec.hpp +65 -0
  81. package/nitrogen/generated/shared/c++/ScrollGuardDirection.hpp +80 -0
  82. package/nitrogen/generated/shared/c++/views/HybridScrollGuardComponent.cpp +87 -0
  83. package/nitrogen/generated/shared/c++/views/HybridScrollGuardComponent.hpp +108 -0
  84. package/nitrogen/generated/shared/json/ScrollGuardConfig.json +10 -0
  85. package/package.json +106 -0
  86. package/src/ScrollGuard.nitro.ts +25 -0
  87. package/src/index.tsx +11 -0
@@ -0,0 +1,29 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "ScrollGuard"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/OneKeyHQ/app-modules.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = [
17
+ "ios/**/*.{swift}",
18
+ "ios/**/*.{m,mm}",
19
+ "cpp/**/*.{hpp,cpp}",
20
+ ]
21
+
22
+ s.dependency 'React-jsi'
23
+ s.dependency 'React-callinvoker'
24
+
25
+ load 'nitrogen/generated/ios/ScrollGuard+autolinking.rb'
26
+ add_nitrogen_files(s)
27
+
28
+ install_modules_dependencies(s)
29
+ end
@@ -0,0 +1,24 @@
1
+ project(scrollguard)
2
+ cmake_minimum_required(VERSION 3.9.0)
3
+
4
+ set(PACKAGE_NAME scrollguard)
5
+ set(CMAKE_VERBOSE_MAKEFILE ON)
6
+ set(CMAKE_CXX_STANDARD 20)
7
+
8
+ # Define C++ library and add all sources
9
+ add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp)
10
+
11
+ # Add Nitrogen specs
12
+ include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/scrollguard+autolinking.cmake)
13
+
14
+ # Set up local includes
15
+ include_directories("src/main/cpp" "../cpp")
16
+
17
+ find_library(LOG_LIB log)
18
+
19
+ # Link all libraries together
20
+ target_link_libraries(
21
+ ${PACKAGE_NAME}
22
+ ${LOG_LIB}
23
+ android
24
+ )
@@ -0,0 +1,124 @@
1
+ buildscript {
2
+ ext.getExtOrDefault = {name ->
3
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['ScrollGuard_' + name]
4
+ }
5
+
6
+ repositories {
7
+ google()
8
+ mavenCentral()
9
+ }
10
+
11
+ dependencies {
12
+ classpath "com.android.tools.build:gradle:8.7.2"
13
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
14
+ }
15
+ }
16
+
17
+ def reactNativeArchitectures() {
18
+ def value = rootProject.getProperties().get("reactNativeArchitectures")
19
+ return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
20
+ }
21
+
22
+ apply plugin: "com.android.library"
23
+ apply plugin: "kotlin-android"
24
+ apply from: '../nitrogen/generated/android/scrollguard+autolinking.gradle'
25
+
26
+ apply plugin: "com.facebook.react"
27
+
28
+ def getExtOrIntegerDefault(name) {
29
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ScrollGuard_" + name]).toInteger()
30
+ }
31
+
32
+ android {
33
+ namespace "com.margelo.nitro.scrollguard"
34
+
35
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
36
+
37
+ defaultConfig {
38
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
39
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
40
+
41
+ externalNativeBuild {
42
+ cmake {
43
+ cppFlags "-frtti -fexceptions -Wall -fstack-protector-all"
44
+ arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
45
+ abiFilters (*reactNativeArchitectures())
46
+
47
+ buildTypes {
48
+ debug {
49
+ cppFlags "-O1 -g"
50
+ }
51
+ release {
52
+ cppFlags "-O2"
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
58
+
59
+ externalNativeBuild {
60
+ cmake {
61
+ path "CMakeLists.txt"
62
+ }
63
+ }
64
+
65
+ packagingOptions {
66
+ excludes = [
67
+ "META-INF",
68
+ "META-INF/**",
69
+ "**/libc++_shared.so",
70
+ "**/libfbjni.so",
71
+ "**/libjsi.so",
72
+ "**/libfolly_json.so",
73
+ "**/libfolly_runtime.so",
74
+ "**/libglog.so",
75
+ "**/libhermes.so",
76
+ "**/libhermes-executor-debug.so",
77
+ "**/libhermes_executor.so",
78
+ "**/libreactnative.so",
79
+ "**/libreactnativejni.so",
80
+ "**/libturbomodulejsijni.so",
81
+ ]
82
+ }
83
+
84
+ buildFeatures {
85
+ prefab true
86
+ }
87
+
88
+ buildTypes {
89
+ release {
90
+ minifyEnabled false
91
+ }
92
+ }
93
+
94
+ lintOptions {
95
+ disable "GradleCompatible"
96
+ }
97
+
98
+ compileOptions {
99
+ sourceCompatibility JavaVersion.VERSION_1_8
100
+ targetCompatibility JavaVersion.VERSION_1_8
101
+ }
102
+
103
+ sourceSets {
104
+ main {
105
+ java.srcDirs += [
106
+ "generated/java",
107
+ "generated/jni"
108
+ ]
109
+ }
110
+ }
111
+ }
112
+
113
+ repositories {
114
+ mavenCentral()
115
+ google()
116
+ }
117
+
118
+ def kotlin_version = getExtOrDefault("kotlinVersion")
119
+
120
+ dependencies {
121
+ implementation "com.facebook.react:react-android"
122
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
123
+ implementation project(":react-native-nitro-modules")
124
+ }
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,6 @@
1
+ #include <jni.h>
2
+ #include "scrollguardOnLoad.hpp"
3
+
4
+ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
5
+ return margelo::nitro::scrollguard::initialize(vm);
6
+ }
@@ -0,0 +1,117 @@
1
+ package com.margelo.nitro.scrollguard
2
+
3
+ import android.content.Context
4
+ import android.view.MotionEvent
5
+ import android.view.View
6
+ import android.view.ViewConfiguration
7
+ import android.widget.FrameLayout
8
+ import com.facebook.proguard.annotations.DoNotStrip
9
+ import com.facebook.react.uimanager.ThemedReactContext
10
+ import kotlin.math.absoluteValue
11
+
12
+ @DoNotStrip
13
+ class HybridScrollGuard(val context: ThemedReactContext) : HybridScrollGuardSpec() {
14
+
15
+ override val view: View = ScrollGuardFrameLayout(context)
16
+
17
+ override var direction: ScrollGuardDirection? = ScrollGuardDirection.HORIZONTAL
18
+ set(value) {
19
+ field = value
20
+ (view as ScrollGuardFrameLayout).guardDirection = value ?: ScrollGuardDirection.HORIZONTAL
21
+ }
22
+ }
23
+
24
+ /**
25
+ * A native FrameLayout that prevents ancestor ViewPager2 (PagerView) from
26
+ * intercepting touch events that belong to child scrollable views.
27
+ *
28
+ * Mechanism: On ACTION_DOWN, immediately calls requestDisallowInterceptTouchEvent(true)
29
+ * to prevent all ancestors from intercepting. On ACTION_MOVE, checks the gesture
30
+ * direction and only keeps blocking for the guarded direction. Vertical gestures
31
+ * are released so the page can still scroll vertically.
32
+ */
33
+ class ScrollGuardFrameLayout(context: Context) : FrameLayout(context) {
34
+
35
+ var guardDirection: ScrollGuardDirection = ScrollGuardDirection.HORIZONTAL
36
+
37
+ private var initialX = 0f
38
+ private var initialY = 0f
39
+ private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
40
+ private var directionDecided = false
41
+
42
+ override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
43
+ when (ev.action) {
44
+ MotionEvent.ACTION_DOWN -> {
45
+ initialX = ev.x
46
+ initialY = ev.y
47
+ directionDecided = false
48
+ // Immediately prevent all ancestors (including ViewPager2) from intercepting.
49
+ // This is critical: ViewPager2's onInterceptTouchEvent runs in the same
50
+ // touch dispatch pass, so we must disallow BEFORE it gets a chance to decide.
51
+ parent?.requestDisallowInterceptTouchEvent(true)
52
+ }
53
+ MotionEvent.ACTION_MOVE -> {
54
+ if (!directionDecided) {
55
+ val dx = (ev.x - initialX).absoluteValue
56
+ val dy = (ev.y - initialY).absoluteValue
57
+
58
+ if (dx > touchSlop || dy > touchSlop) {
59
+ directionDecided = true
60
+ val isHorizontalGesture = dx > dy
61
+
62
+ val shouldBlock = when (guardDirection) {
63
+ ScrollGuardDirection.HORIZONTAL -> isHorizontalGesture
64
+ ScrollGuardDirection.VERTICAL -> !isHorizontalGesture
65
+ ScrollGuardDirection.BOTH -> true
66
+ }
67
+
68
+ if (shouldBlock) {
69
+ // Guarded direction: keep blocking ancestors
70
+ parent?.requestDisallowInterceptTouchEvent(true)
71
+ } else {
72
+ // Non-guarded direction: release to ancestors
73
+ // (e.g., allow vertical scrolling of collapsible tabs)
74
+ parent?.requestDisallowInterceptTouchEvent(false)
75
+ }
76
+ }
77
+ }
78
+ }
79
+ MotionEvent.ACTION_UP,
80
+ MotionEvent.ACTION_CANCEL -> {
81
+ directionDecided = false
82
+ }
83
+ }
84
+ // Never intercept: let children (ScrollView/FlatList) handle touches normally
85
+ return false
86
+ }
87
+
88
+ /**
89
+ * Re-assert disallow after child dispatch.
90
+ *
91
+ * This handles the case where a deeply-nested NestedScrollableHost (from another
92
+ * PagerView) calls requestDisallowInterceptTouchEvent(false) during its own
93
+ * dispatchTouchEvent, which would propagate up and override our earlier disallow.
94
+ */
95
+ override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
96
+ val handled = super.dispatchTouchEvent(ev)
97
+
98
+ if (ev.action == MotionEvent.ACTION_MOVE && directionDecided) {
99
+ val dx = (ev.x - initialX).absoluteValue
100
+ val dy = (ev.y - initialY).absoluteValue
101
+ val isHorizontalGesture = dx > dy
102
+
103
+ val shouldBlock = when (guardDirection) {
104
+ ScrollGuardDirection.HORIZONTAL -> isHorizontalGesture
105
+ ScrollGuardDirection.VERTICAL -> !isHorizontalGesture
106
+ ScrollGuardDirection.BOTH -> true
107
+ }
108
+
109
+ if (shouldBlock) {
110
+ // Re-assert after child dispatch to prevent override
111
+ parent?.requestDisallowInterceptTouchEvent(true)
112
+ }
113
+ }
114
+
115
+ return handled
116
+ }
117
+ }
@@ -0,0 +1,27 @@
1
+ package com.margelo.nitro.scrollguard
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfoProvider
7
+ import com.facebook.react.uimanager.ViewManager
8
+
9
+ class ScrollGuardPackage : BaseReactPackage() {
10
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
11
+ return null
12
+ }
13
+
14
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
15
+ return ReactModuleInfoProvider { HashMap() }
16
+ }
17
+
18
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
19
+ return listOf(ScrollGuardViewGroupManager())
20
+ }
21
+
22
+ companion object {
23
+ init {
24
+ System.loadLibrary("scrollguard")
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,48 @@
1
+ package com.margelo.nitro.scrollguard
2
+
3
+ import android.view.View
4
+ import com.facebook.react.uimanager.ReactStylesDiffMap
5
+ import com.facebook.react.uimanager.StateWrapper
6
+ import com.facebook.react.uimanager.ThemedReactContext
7
+ import com.facebook.react.uimanager.ViewGroupManager
8
+ import com.margelo.nitro.scrollguard.views.HybridScrollGuardStateUpdater
9
+
10
+ /**
11
+ * Custom ViewGroupManager for ScrollGuard that supports child views.
12
+ *
13
+ * The nitrogen-generated HybridScrollGuardManager extends SimpleViewManager,
14
+ * which does not implement IViewGroupManager. Since ScrollGuardFrameLayout
15
+ * is a FrameLayout that wraps child scrollable views, we need a ViewGroupManager.
16
+ */
17
+ class ScrollGuardViewGroupManager : ViewGroupManager<ScrollGuardFrameLayout>() {
18
+ private val views = hashMapOf<View, HybridScrollGuard>()
19
+
20
+ override fun getName(): String = "ScrollGuard"
21
+
22
+ override fun createViewInstance(reactContext: ThemedReactContext): ScrollGuardFrameLayout {
23
+ val hybridView = HybridScrollGuard(reactContext)
24
+ val view = hybridView.view as ScrollGuardFrameLayout
25
+ views[view] = hybridView
26
+ return view
27
+ }
28
+
29
+ override fun onDropViewInstance(view: ScrollGuardFrameLayout) {
30
+ super.onDropViewInstance(view)
31
+ views.remove(view)
32
+ }
33
+
34
+ override fun updateState(
35
+ view: ScrollGuardFrameLayout,
36
+ props: ReactStylesDiffMap,
37
+ stateWrapper: StateWrapper
38
+ ): Any? {
39
+ val hybridView = views[view]
40
+ ?: throw Error("Couldn't find view $view in local views table!")
41
+
42
+ hybridView.beforeUpdate()
43
+ HybridScrollGuardStateUpdater.updateViewProps(hybridView, stateWrapper)
44
+ hybridView.afterUpdate()
45
+
46
+ return super.updateState(view, props, stateWrapper)
47
+ }
48
+ }
@@ -0,0 +1,243 @@
1
+ import Foundation
2
+ import UIKit
3
+
4
+ class HybridScrollGuard: HybridScrollGuardSpec {
5
+
6
+ var view: UIView = ScrollGuardView()
7
+
8
+ var direction: ScrollGuardDirection? = .horizontal {
9
+ didSet {
10
+ (view as? ScrollGuardView)?.guardDirection = direction ?? .horizontal
11
+ }
12
+ }
13
+
14
+ override init() {
15
+ super.init()
16
+ }
17
+ }
18
+
19
+ // MARK: - ScrollGuardView
20
+
21
+ /// Prevents ancestor PagerView from intercepting scroll gestures belonging to
22
+ /// a sibling UIScrollView.
23
+ ///
24
+ /// Two-layer blocking strategy:
25
+ /// 1. A "touch detector" (UILongPressGestureRecognizer with minimumPressDuration=0)
26
+ /// fires on touch-down IMMEDIATELY — before any pan gesture — and disables the
27
+ /// pager's isScrollEnabled. This eliminates first-touch-after-page-transition bugs.
28
+ /// 2. A "blocker" UIPanGestureRecognizer with require(toFail:) for belt-and-suspenders.
29
+ ///
30
+ /// Both gestures live on the CHILD ScrollView (Nitro host views render React
31
+ /// children as native siblings, not subviews).
32
+ class ScrollGuardView: UIView, UIGestureRecognizerDelegate {
33
+
34
+ var guardDirection: ScrollGuardDirection = .horizontal {
35
+ didSet {
36
+ invalidateSetup()
37
+ }
38
+ }
39
+
40
+ private weak var connectedPager: UIScrollView?
41
+ private weak var connectedChild: UIScrollView?
42
+ private var blockerGesture: UIPanGestureRecognizer?
43
+ private var touchDetector: UILongPressGestureRecognizer?
44
+ private var pagerScrollEnabledOriginal: Bool = true
45
+
46
+ deinit {
47
+ cleanupConnection()
48
+ }
49
+
50
+ // MARK: - Touch pass-through
51
+
52
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
53
+ return nil
54
+ }
55
+
56
+ // MARK: - Lifecycle
57
+
58
+ override func didMoveToWindow() {
59
+ super.didMoveToWindow()
60
+ if window != nil {
61
+ setupGestureBlocking()
62
+ if connectedChild == nil {
63
+ DispatchQueue.main.async { [weak self] in
64
+ self?.setupGestureBlocking()
65
+ }
66
+ }
67
+ }
68
+ // Do NOT clean up on window=nil — child persists across pager transitions.
69
+ }
70
+
71
+ override func layoutSubviews() {
72
+ super.layoutSubviews()
73
+ if connectedChild == nil && window != nil {
74
+ setupGestureBlocking()
75
+ }
76
+ }
77
+
78
+ private func invalidateSetup() {
79
+ cleanupConnection()
80
+ if window != nil {
81
+ DispatchQueue.main.async { [weak self] in
82
+ self?.setupGestureBlocking()
83
+ }
84
+ }
85
+ }
86
+
87
+ private func cleanupConnection() {
88
+ if let child = connectedChild {
89
+ if let blocker = blockerGesture { child.removeGestureRecognizer(blocker) }
90
+ if let detector = touchDetector { child.removeGestureRecognizer(detector) }
91
+ }
92
+ // Restore pager scrolling if we disabled it
93
+ if let pager = connectedPager {
94
+ pager.isScrollEnabled = pagerScrollEnabledOriginal
95
+ }
96
+ blockerGesture = nil
97
+ touchDetector = nil
98
+ connectedPager = nil
99
+ connectedChild = nil
100
+ }
101
+
102
+ // MARK: - Setup
103
+
104
+ private func setupGestureBlocking() {
105
+ guard window != nil else { return }
106
+ guard let pagerSV = findAncestorPagingScrollView() else { return }
107
+ guard let childSV = findSiblingScrollView() else { return }
108
+
109
+ // Skip if already connected to the same child with gestures still attached
110
+ if connectedChild === childSV,
111
+ let blocker = blockerGesture,
112
+ let detector = touchDetector,
113
+ childSV.gestureRecognizers?.contains(blocker) == true,
114
+ childSV.gestureRecognizers?.contains(detector) == true {
115
+ return
116
+ }
117
+
118
+ cleanupConnection()
119
+
120
+ // 1) Touch detector — fires on touch-down BEFORE any pan gesture.
121
+ // Immediately disables pager scrolling so it can't intercept.
122
+ let detector = UILongPressGestureRecognizer(target: self, action: #selector(handleTouchDetected(_:)))
123
+ detector.minimumPressDuration = 0
124
+ detector.cancelsTouchesInView = false
125
+ detector.delaysTouchesBegan = false
126
+ detector.delaysTouchesEnded = false
127
+ detector.delegate = self
128
+ childSV.addGestureRecognizer(detector)
129
+
130
+ // 2) Blocker pan gesture — belt-and-suspenders with require(toFail:).
131
+ let blocker = UIPanGestureRecognizer(target: self, action: #selector(handleBlockerPan(_:)))
132
+ blocker.cancelsTouchesInView = false
133
+ blocker.delaysTouchesBegan = false
134
+ blocker.delaysTouchesEnded = false
135
+ blocker.delegate = self
136
+ childSV.addGestureRecognizer(blocker)
137
+
138
+ // All pager gestures require blocker to fail
139
+ if let pagerGestures = pagerSV.gestureRecognizers {
140
+ for gesture in pagerGestures {
141
+ gesture.require(toFail: blocker)
142
+ }
143
+ }
144
+
145
+ blockerGesture = blocker
146
+ touchDetector = detector
147
+ connectedPager = pagerSV
148
+ connectedChild = childSV
149
+ pagerScrollEnabledOriginal = pagerSV.isScrollEnabled
150
+ }
151
+
152
+ // MARK: - Touch detector handler
153
+
154
+ @objc private func handleTouchDetected(_ gesture: UILongPressGestureRecognizer) {
155
+ guard let pager = connectedPager else { return }
156
+ switch gesture.state {
157
+ case .began:
158
+ // Touch down — immediately disable pager
159
+ pagerScrollEnabledOriginal = pager.isScrollEnabled
160
+ pager.isScrollEnabled = false
161
+ case .ended, .cancelled, .failed:
162
+ // Touch up — restore pager
163
+ pager.isScrollEnabled = pagerScrollEnabledOriginal
164
+ default:
165
+ break
166
+ }
167
+ }
168
+
169
+ // MARK: - Blocker pan handler
170
+
171
+ @objc private func handleBlockerPan(_ gesture: UIPanGestureRecognizer) {
172
+ // Empty — exists only for require(toFail:) blocking
173
+ }
174
+
175
+ // MARK: - UIGestureRecognizerDelegate
176
+
177
+ func gestureRecognizer(
178
+ _ gestureRecognizer: UIGestureRecognizer,
179
+ shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
180
+ ) -> Bool {
181
+ return true
182
+ }
183
+
184
+ override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
185
+ // Touch detector always begins (it handles all touches in the child area)
186
+ if gestureRecognizer === touchDetector { return true }
187
+
188
+ // Blocker only begins for guarded direction
189
+ guard gestureRecognizer === blockerGesture,
190
+ let pan = gestureRecognizer as? UIPanGestureRecognizer else {
191
+ return true
192
+ }
193
+ let velocity = pan.velocity(in: pan.view)
194
+ switch guardDirection {
195
+ case .horizontal:
196
+ return abs(velocity.x) > abs(velocity.y)
197
+ case .vertical:
198
+ return abs(velocity.y) > abs(velocity.x)
199
+ case .both:
200
+ return true
201
+ }
202
+ }
203
+
204
+ // MARK: - View hierarchy search
205
+
206
+ private func findAncestorPagingScrollView() -> UIScrollView? {
207
+ var current: UIView? = superview
208
+ while let v = current {
209
+ if let sv = v as? UIScrollView, sv.isPagingEnabled { return sv }
210
+ current = v.superview
211
+ }
212
+ return nil
213
+ }
214
+
215
+ private func findSiblingScrollView() -> UIScrollView? {
216
+ guard let parent = superview else { return nil }
217
+
218
+ var foundSelf = false
219
+ for subview in parent.subviews {
220
+ if subview === self { foundSelf = true; continue }
221
+ if foundSelf {
222
+ if let sv = findFirstScrollView(in: subview) { return sv }
223
+ }
224
+ }
225
+
226
+ let myFrame = self.frame
227
+ for subview in parent.subviews {
228
+ if subview === self { continue }
229
+ if subview.frame.intersects(myFrame) {
230
+ if let sv = findFirstScrollView(in: subview) { return sv }
231
+ }
232
+ }
233
+ return nil
234
+ }
235
+
236
+ private func findFirstScrollView(in view: UIView) -> UIScrollView? {
237
+ if let sv = view as? UIScrollView { return sv }
238
+ for subview in view.subviews {
239
+ if let sv = findFirstScrollView(in: subview) { return sv }
240
+ }
241
+ return nil
242
+ }
243
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+
3
+ export let ScrollGuardDirection = /*#__PURE__*/function (ScrollGuardDirection) {
4
+ ScrollGuardDirection["HORIZONTAL"] = "horizontal";
5
+ ScrollGuardDirection["VERTICAL"] = "vertical";
6
+ ScrollGuardDirection["BOTH"] = "both";
7
+ return ScrollGuardDirection;
8
+ }({});
9
+ //# sourceMappingURL=ScrollGuard.nitro.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["ScrollGuardDirection"],"sourceRoot":"../../src","sources":["ScrollGuard.nitro.ts"],"mappings":";;AAMA,WAAYA,oBAAoB,0BAApBA,oBAAoB;EAApBA,oBAAoB;EAApBA,oBAAoB;EAApBA,oBAAoB;EAAA,OAApBA,oBAAoB;AAAA","ignoreList":[]}
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+
3
+ import { getHostComponent } from 'react-native-nitro-modules';
4
+ const ScrollGuardConfig = require('../nitrogen/generated/shared/json/ScrollGuardConfig.json');
5
+ export { ScrollGuardDirection } from "./ScrollGuard.nitro.js";
6
+ export const ScrollGuardView = getHostComponent('ScrollGuard', () => ScrollGuardConfig);
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["getHostComponent","ScrollGuardConfig","require","ScrollGuardDirection","ScrollGuardView"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,gBAAgB,QAAQ,4BAA4B;AAC7D,MAAMC,iBAAiB,GAAGC,OAAO,CAAC,0DAA0D,CAAC;AAE7F,SAASC,oBAAoB,QAAQ,wBAAqB;AAE1D,OAAO,MAAMC,eAAe,GAAGJ,gBAAgB,CAG7C,aAAa,EAAE,MAAMC,iBAAiB,CAAC","ignoreList":[]}
@@ -0,0 +1 @@
1
+ {"type":"module"}