@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.
- package/ScrollGuard.podspec +29 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +124 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/scrollguard/ScrollGuard.kt +117 -0
- package/android/src/main/java/com/margelo/nitro/scrollguard/ScrollGuardPackage.kt +27 -0
- package/android/src/main/java/com/margelo/nitro/scrollguard/ScrollGuardViewGroupManager.kt +48 -0
- package/ios/ScrollGuard.swift +243 -0
- package/lib/module/ScrollGuard.nitro.js +9 -0
- package/lib/module/ScrollGuard.nitro.js.map +1 -0
- package/lib/module/index.js +7 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/nitrogen/generated/android/c++/JHybridScrollGuardSpec.cpp +59 -0
- package/lib/nitrogen/generated/android/c++/JHybridScrollGuardSpec.hpp +66 -0
- package/lib/nitrogen/generated/android/c++/JScrollGuardDirection.hpp +62 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridScrollGuardStateUpdater.cpp +56 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridScrollGuardStateUpdater.hpp +49 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/HybridScrollGuardSpec.kt +59 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/ScrollGuardDirection.kt +22 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/scrollguardOnLoad.kt +35 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/views/HybridScrollGuardManager.kt +50 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/views/HybridScrollGuardStateUpdater.kt +23 -0
- package/lib/nitrogen/generated/android/scrollguard+autolinking.cmake +83 -0
- package/lib/nitrogen/generated/android/scrollguard+autolinking.gradle +27 -0
- package/lib/nitrogen/generated/android/scrollguardOnLoad.cpp +46 -0
- package/lib/nitrogen/generated/android/scrollguardOnLoad.hpp +25 -0
- package/lib/nitrogen/generated/ios/ScrollGuard+autolinking.rb +60 -0
- package/lib/nitrogen/generated/ios/ScrollGuard-Swift-Cxx-Bridge.cpp +33 -0
- package/lib/nitrogen/generated/ios/ScrollGuard-Swift-Cxx-Bridge.hpp +59 -0
- package/lib/nitrogen/generated/ios/ScrollGuard-Swift-Cxx-Umbrella.hpp +45 -0
- package/lib/nitrogen/generated/ios/ScrollGuardAutolinking.mm +33 -0
- package/lib/nitrogen/generated/ios/ScrollGuardAutolinking.swift +25 -0
- package/lib/nitrogen/generated/ios/c++/HybridScrollGuardSpecSwift.cpp +11 -0
- package/lib/nitrogen/generated/ios/c++/HybridScrollGuardSpecSwift.hpp +77 -0
- package/lib/nitrogen/generated/ios/c++/views/HybridScrollGuardComponent.mm +96 -0
- package/lib/nitrogen/generated/ios/swift/HybridScrollGuardSpec.swift +56 -0
- package/lib/nitrogen/generated/ios/swift/HybridScrollGuardSpec_cxx.swift +146 -0
- package/lib/nitrogen/generated/ios/swift/ScrollGuardDirection.swift +44 -0
- package/lib/nitrogen/generated/shared/c++/HybridScrollGuardSpec.cpp +22 -0
- package/lib/nitrogen/generated/shared/c++/HybridScrollGuardSpec.hpp +65 -0
- package/lib/nitrogen/generated/shared/c++/ScrollGuardDirection.hpp +80 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridScrollGuardComponent.cpp +87 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridScrollGuardComponent.hpp +108 -0
- package/lib/nitrogen/generated/shared/json/ScrollGuardConfig.json +10 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/ScrollGuard.nitro.d.ts +19 -0
- package/lib/typescript/src/ScrollGuard.nitro.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +17 -0
- package/nitrogen/generated/android/c++/JHybridScrollGuardSpec.cpp +59 -0
- package/nitrogen/generated/android/c++/JHybridScrollGuardSpec.hpp +66 -0
- package/nitrogen/generated/android/c++/JScrollGuardDirection.hpp +62 -0
- package/nitrogen/generated/android/c++/views/JHybridScrollGuardStateUpdater.cpp +56 -0
- package/nitrogen/generated/android/c++/views/JHybridScrollGuardStateUpdater.hpp +49 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/HybridScrollGuardSpec.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/ScrollGuardDirection.kt +22 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/scrollguardOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/views/HybridScrollGuardManager.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/scrollguard/views/HybridScrollGuardStateUpdater.kt +23 -0
- package/nitrogen/generated/android/scrollguard+autolinking.cmake +83 -0
- package/nitrogen/generated/android/scrollguard+autolinking.gradle +27 -0
- package/nitrogen/generated/android/scrollguardOnLoad.cpp +46 -0
- package/nitrogen/generated/android/scrollguardOnLoad.hpp +25 -0
- package/nitrogen/generated/ios/ScrollGuard+autolinking.rb +60 -0
- package/nitrogen/generated/ios/ScrollGuard-Swift-Cxx-Bridge.cpp +33 -0
- package/nitrogen/generated/ios/ScrollGuard-Swift-Cxx-Bridge.hpp +59 -0
- package/nitrogen/generated/ios/ScrollGuard-Swift-Cxx-Umbrella.hpp +45 -0
- package/nitrogen/generated/ios/ScrollGuardAutolinking.mm +33 -0
- package/nitrogen/generated/ios/ScrollGuardAutolinking.swift +25 -0
- package/nitrogen/generated/ios/c++/HybridScrollGuardSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridScrollGuardSpecSwift.hpp +77 -0
- package/nitrogen/generated/ios/c++/views/HybridScrollGuardComponent.mm +96 -0
- package/nitrogen/generated/ios/swift/HybridScrollGuardSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridScrollGuardSpec_cxx.swift +146 -0
- package/nitrogen/generated/ios/swift/ScrollGuardDirection.swift +44 -0
- package/nitrogen/generated/shared/c++/HybridScrollGuardSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridScrollGuardSpec.hpp +65 -0
- package/nitrogen/generated/shared/c++/ScrollGuardDirection.hpp +80 -0
- package/nitrogen/generated/shared/c++/views/HybridScrollGuardComponent.cpp +87 -0
- package/nitrogen/generated/shared/c++/views/HybridScrollGuardComponent.hpp +108 -0
- package/nitrogen/generated/shared/json/ScrollGuardConfig.json +10 -0
- package/package.json +106 -0
- package/src/ScrollGuard.nitro.ts +25 -0
- 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,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"}
|