expo-scroll-forwarder 0.1.6 → 1.0.0
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/ExpoScrollForwarder.podspec +17 -0
- package/LICENSE +0 -0
- package/README.md +30 -182
- package/expo-module.config.json +6 -9
- package/index.ts +1 -0
- package/package.json +33 -43
- package/src/ExpoScrollForwarder.types.ts +4 -4
- package/src/ExpoScrollForwarderView.android.tsx +8 -0
- package/src/ExpoScrollForwarderView.ios.tsx +13 -0
- package/src/ExpoScrollForwarderView.tsx +8 -13
- package/{ios → src/ios}/ExpoScrollForwarderModule.swift +13 -13
- package/{ios → src/ios}/ExpoScrollForwarderView.swift +138 -221
- package/.eslintrc.js +0 -5
- package/android/build.gradle +0 -48
- package/android/src/main/AndroidManifest.xml +0 -2
- package/android/src/main/java/expo/modules/scrollforwarder/ExpoScrollForwarderModule.kt +0 -17
- package/android/src/main/java/expo/modules/scrollforwarder/ExpoScrollForwarderView.kt +0 -223
- package/build/ExpoScrollForwarder.types.d.ts +0 -5
- package/build/ExpoScrollForwarder.types.d.ts.map +0 -1
- package/build/ExpoScrollForwarder.types.js +0 -2
- package/build/ExpoScrollForwarder.types.js.map +0 -1
- package/build/ExpoScrollForwarderView.d.ts +0 -4
- package/build/ExpoScrollForwarderView.d.ts.map +0 -1
- package/build/ExpoScrollForwarderView.js +0 -7
- package/build/ExpoScrollForwarderView.js.map +0 -1
- package/build/index.d.ts +0 -2
- package/build/index.d.ts.map +0 -1
- package/build/index.js +0 -2
- package/build/index.js.map +0 -1
- package/ios/ExpoScrollForwarder.podspec +0 -29
- package/src/index.ts +0 -1
- package/tsconfig.json +0 -9
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Pod::Spec.new do |s|
|
|
2
|
+
s.name = 'ExpoScrollForwarder'
|
|
3
|
+
s.version = '1.0.0'
|
|
4
|
+
s.summary = 'Forward scroll gesture from UIView to UIScrollView'
|
|
5
|
+
s.description = 'Forward scroll gesture from UIView to UIScrollView'
|
|
6
|
+
s.author = 'Sharif Rayhan Nafi'
|
|
7
|
+
s.homepage = 'https://github.com/sharifrayhan/expo-scroll-forwarder'
|
|
8
|
+
s.platforms = { :ios => '13.4' }
|
|
9
|
+
s.source = { git: '', tag: s.version }
|
|
10
|
+
s.static_framework = true
|
|
11
|
+
s.dependency 'ExpoModulesCore'
|
|
12
|
+
s.pod_target_xcconfig = {
|
|
13
|
+
'DEFINES_MODULE' => 'YES',
|
|
14
|
+
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
|
15
|
+
}
|
|
16
|
+
s.source_files = "src/ios/*.{h,m,mm,swift,hpp,cpp}"
|
|
17
|
+
end
|
package/LICENSE
ADDED
|
File without changes
|
package/README.md
CHANGED
|
@@ -1,182 +1,30 @@
|
|
|
1
|
-
# expo-scroll-forwarder
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
findNodeHandle,
|
|
32
|
-
} from "react-native";
|
|
33
|
-
|
|
34
|
-
export default function App() {
|
|
35
|
-
const scrollViewRef = useRef<ScrollView>(null);
|
|
36
|
-
const [scrollViewTag, setScrollViewTag] = useState<number | null>(null);
|
|
37
|
-
const [refreshing, setRefreshing] = useState(false);
|
|
38
|
-
|
|
39
|
-
// Get the native tag of the ScrollView
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
if (scrollViewRef.current) {
|
|
42
|
-
const tag = findNodeHandle(scrollViewRef.current);
|
|
43
|
-
setScrollViewTag(tag);
|
|
44
|
-
}
|
|
45
|
-
}, []);
|
|
46
|
-
|
|
47
|
-
const onRefresh = () => {
|
|
48
|
-
setRefreshing(true);
|
|
49
|
-
setTimeout(() => setRefreshing(false), 2000);
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<>
|
|
54
|
-
{/* Header that forwards scroll gestures */}
|
|
55
|
-
<ExpoScrollForwarderView scrollViewTag={scrollViewTag}>
|
|
56
|
-
<View style={{ padding: 20, backgroundColor: "#6366f1" }}>
|
|
57
|
-
<Text style={{ color: "white", fontSize: 20 }}>
|
|
58
|
-
Swipe down here to scroll
|
|
59
|
-
</Text>
|
|
60
|
-
</View>
|
|
61
|
-
</ExpoScrollForwarderView>
|
|
62
|
-
|
|
63
|
-
{/* Main ScrollView */}
|
|
64
|
-
<ScrollView
|
|
65
|
-
ref={scrollViewRef}
|
|
66
|
-
refreshControl={
|
|
67
|
-
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
|
68
|
-
}
|
|
69
|
-
>
|
|
70
|
-
<View style={{ padding: 20 }}>
|
|
71
|
-
<Text>Your scrollable content here</Text>
|
|
72
|
-
{/* Add more content... */}
|
|
73
|
-
</View>
|
|
74
|
-
</ScrollView>
|
|
75
|
-
</>
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## API Reference
|
|
81
|
-
|
|
82
|
-
### ExpoScrollForwarderView
|
|
83
|
-
|
|
84
|
-
A view component that forwards scroll gestures to a target ScrollView.
|
|
85
|
-
|
|
86
|
-
#### Props
|
|
87
|
-
|
|
88
|
-
| Prop | Type | Required | Description |
|
|
89
|
-
| --------------- | ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
90
|
-
| `scrollViewTag` | `number \| null` | Yes | The native tag of the ScrollView to forward gestures to. Obtain this using `findNodeHandle(scrollViewRef.current)` |
|
|
91
|
-
| `children` | `React.ReactNode` | Yes | The content that will capture and forward scroll gestures |
|
|
92
|
-
|
|
93
|
-
## How It Works
|
|
94
|
-
|
|
95
|
-
1. **Get ScrollView Tag**: Use `findNodeHandle()` to get the native tag of your ScrollView
|
|
96
|
-
2. **Wrap Your Header**: Wrap any view in `ExpoScrollForwarderView` and pass the tag
|
|
97
|
-
3. **Gesture Forwarding**: Touch gestures on the wrapped view are forwarded to the ScrollView
|
|
98
|
-
4. **Natural Scrolling**: Includes momentum, damping, and all native scroll behaviors
|
|
99
|
-
|
|
100
|
-
## Use Cases
|
|
101
|
-
|
|
102
|
-
- **Collapsible Headers**: Create headers that users can swipe to scroll the content below
|
|
103
|
-
- **Custom Navigation Bars**: Make navigation bars scrollable
|
|
104
|
-
- **Dashboard Cards**: Allow cards or panels to initiate scrolling
|
|
105
|
-
- **Video Player Controls**: Overlay controls that don't block scroll gestures
|
|
106
|
-
- **Chat Input Areas**: Input fields that can still control chat scroll
|
|
107
|
-
|
|
108
|
-
## Platform Support
|
|
109
|
-
|
|
110
|
-
- ✅ iOS 13.4+
|
|
111
|
-
- ✅ Android API 21+
|
|
112
|
-
- ✅ Expo SDK 50+
|
|
113
|
-
|
|
114
|
-
## Example
|
|
115
|
-
|
|
116
|
-
Check out the [example app](./example) in the repository for a full working demo.
|
|
117
|
-
|
|
118
|
-
## Technical Details
|
|
119
|
-
|
|
120
|
-
### iOS Implementation
|
|
121
|
-
|
|
122
|
-
- Uses `UIPanGestureRecognizer` for gesture detection
|
|
123
|
-
- Custom decay animation with configurable friction
|
|
124
|
-
- Integrates with native `UIScrollView` and `RCTRefreshControl`
|
|
125
|
-
- Prevents conflicts with swipe-back gestures
|
|
126
|
-
|
|
127
|
-
### Android Implementation
|
|
128
|
-
|
|
129
|
-
- Uses `GestureDetector` and custom touch event handling
|
|
130
|
-
- `ValueAnimator` for smooth decay animations
|
|
131
|
-
- Integrates with `ReactScrollView` and RefreshControl
|
|
132
|
-
- Proper velocity tracking and momentum calculation
|
|
133
|
-
|
|
134
|
-
## Troubleshooting
|
|
135
|
-
|
|
136
|
-
### ScrollView not responding to gestures
|
|
137
|
-
|
|
138
|
-
Make sure you're correctly getting and setting the `scrollViewTag`:
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
const tag = findNodeHandle(scrollViewRef.current);
|
|
142
|
-
setScrollViewTag(tag);
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### Gestures interfering with other touch handlers
|
|
146
|
-
|
|
147
|
-
The module automatically handles gesture conflicts, but ensure your view hierarchy is correct - the `ExpoScrollForwarderView` should be a sibling or parent of the ScrollView, not a child.
|
|
148
|
-
|
|
149
|
-
### Pull-to-refresh not working
|
|
150
|
-
|
|
151
|
-
Ensure you've added a `RefreshControl` to your ScrollView:
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
<ScrollView
|
|
155
|
-
refreshControl={
|
|
156
|
-
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
|
157
|
-
}
|
|
158
|
-
>
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
## Contributing
|
|
162
|
-
|
|
163
|
-
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
164
|
-
|
|
165
|
-
## License
|
|
166
|
-
|
|
167
|
-
MIT © [Sharif Rayhan Nafi](https://github.com/sharifrayhan)
|
|
168
|
-
|
|
169
|
-
## Author
|
|
170
|
-
|
|
171
|
-
**Sharif Rayhan Nafi**
|
|
172
|
-
|
|
173
|
-
- GitHub: [@sharifrayhan](https://github.com/sharifrayhan)
|
|
174
|
-
- Email: sharifrayhan.official@gmail.com
|
|
175
|
-
|
|
176
|
-
## Acknowledgments
|
|
177
|
-
|
|
178
|
-
This module was inspired by the scroll forwarding behavior in the Bluesky app
|
|
179
|
-
.
|
|
180
|
-
Special thanks to the Bluesky engineering team — their implementation and user experience provided valuable insight into creating a natural, responsive gesture-forwarding system.
|
|
181
|
-
|
|
182
|
-
Built with [Expo Modules](https://docs.expo.dev/modules/) 🚀
|
|
1
|
+
# expo-scroll-forwarder
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/expo-scroll-forwarder)
|
|
4
|
+
[](https://github.com/sharifrayhan/expo-scroll-forwarder/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
`expo-scroll-forwarder` is an **iOS-only Expo module** that allows you to forward scroll gestures from a native view to a `ScrollView`. This is useful for creating **custom headers, pull-to-refresh areas, or gesture-forwarding components** in React Native and Expo.
|
|
7
|
+
|
|
8
|
+
> ⚠️ Android support will be added in a future release.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- Forward vertical scroll gestures from a native view to a `ScrollView`.
|
|
15
|
+
- Compatible with Expo modules architecture.
|
|
16
|
+
- Pure iOS implementation using Swift and `ExpoModulesCore`.
|
|
17
|
+
- Works with `ScrollView` and `RefreshControl`.
|
|
18
|
+
- Fully typed with TypeScript for React Native.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
Install the package in your Expo or React Native project:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install expo-scroll-forwarder
|
|
28
|
+
# or
|
|
29
|
+
yarn add expo-scroll-forwarder
|
|
30
|
+
```
|
package/expo-module.config.json
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
{
|
|
2
|
-
"platforms": ["
|
|
3
|
-
"
|
|
4
|
-
"modules": ["ExpoScrollForwarderModule"]
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
"modules": ["expo.modules.scrollforwarder.ExpoScrollForwarderModule"]
|
|
8
|
-
}
|
|
9
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"platforms": ["ios"],
|
|
3
|
+
"ios": {
|
|
4
|
+
"modules": ["ExpoScrollForwarderModule"]
|
|
5
|
+
}
|
|
6
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ExpoScrollForwarderView } from "./src/ExpoScrollForwarderView";
|
package/package.json
CHANGED
|
@@ -1,43 +1,33 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "expo-scroll-forwarder",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"react
|
|
20
|
-
"
|
|
21
|
-
"expo-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"expo-module-scripts": "^5.0.7",
|
|
35
|
-
"expo": "^54.0.18",
|
|
36
|
-
"react-native": "0.81.5"
|
|
37
|
-
},
|
|
38
|
-
"peerDependencies": {
|
|
39
|
-
"expo": "*",
|
|
40
|
-
"react": "*",
|
|
41
|
-
"react-native": "*"
|
|
42
|
-
}
|
|
43
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "expo-scroll-forwarder",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "index.ts",
|
|
5
|
+
"types": "index.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"src",
|
|
8
|
+
"index.ts",
|
|
9
|
+
"ExpoScrollForwarder.podspec",
|
|
10
|
+
"expo-module.config.json"
|
|
11
|
+
],
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"react": "*",
|
|
14
|
+
"react-native": "*",
|
|
15
|
+
"expo-modules-core": "*"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"typescript": "^5.2.0",
|
|
19
|
+
"@types/react": "^18.2.0",
|
|
20
|
+
"@types/react-native": "^0.72.0",
|
|
21
|
+
"expo-modules-core": "*"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc --build"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"expo",
|
|
28
|
+
"react-native",
|
|
29
|
+
"scroll",
|
|
30
|
+
"forwarder",
|
|
31
|
+
"module"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export interface ExpoScrollForwarderViewProps {
|
|
2
|
-
scrollViewTag: number | null
|
|
3
|
-
children: React.ReactNode
|
|
4
|
-
}
|
|
1
|
+
export interface ExpoScrollForwarderViewProps {
|
|
2
|
+
scrollViewTag: number | null;
|
|
3
|
+
children: React.ReactNode;
|
|
4
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { requireNativeViewManager } from "expo-modules-core";
|
|
3
|
+
import { ExpoScrollForwarderViewProps } from "./ExpoScrollForwarder.types";
|
|
4
|
+
|
|
5
|
+
const NativeView: React.ComponentType<ExpoScrollForwarderViewProps> =
|
|
6
|
+
requireNativeViewManager("ExpoScrollForwarder");
|
|
7
|
+
|
|
8
|
+
export function ExpoScrollForwarderView({
|
|
9
|
+
children,
|
|
10
|
+
...rest
|
|
11
|
+
}: ExpoScrollForwarderViewProps) {
|
|
12
|
+
return <NativeView {...rest}>{children}</NativeView>;
|
|
13
|
+
}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
children,
|
|
10
|
-
...rest
|
|
11
|
-
}: ExpoScrollForwarderViewProps) {
|
|
12
|
-
return <NativeView {...rest}>{children}</NativeView>;
|
|
13
|
-
}
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ExpoScrollForwarderViewProps } from "./ExpoScrollForwarder.types";
|
|
3
|
+
|
|
4
|
+
export function ExpoScrollForwarderView({
|
|
5
|
+
children,
|
|
6
|
+
}: ExpoScrollForwarderViewProps) {
|
|
7
|
+
return children; // cross-platform fallback
|
|
8
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import ExpoModulesCore
|
|
2
|
-
|
|
3
|
-
public class ExpoScrollForwarderModule: Module {
|
|
4
|
-
public func definition() -> ModuleDefinition {
|
|
5
|
-
Name("ExpoScrollForwarder")
|
|
6
|
-
|
|
7
|
-
View(ExpoScrollForwarderView.self) {
|
|
8
|
-
Prop("scrollViewTag") { (view: ExpoScrollForwarderView, prop: Int) in
|
|
9
|
-
view.scrollViewTag = prop
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
}
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
public class ExpoScrollForwarderModule: Module {
|
|
4
|
+
public func definition() -> ModuleDefinition {
|
|
5
|
+
Name("ExpoScrollForwarder")
|
|
6
|
+
|
|
7
|
+
View(ExpoScrollForwarderView.self) {
|
|
8
|
+
Prop("scrollViewTag") { (view: ExpoScrollForwarderView, prop: Int) in
|
|
9
|
+
view.scrollViewTag = prop
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -1,221 +1,138 @@
|
|
|
1
|
-
import ExpoModulesCore
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
private var
|
|
13
|
-
private var
|
|
14
|
-
private var
|
|
15
|
-
private var
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
self
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
lpg.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
self.animTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 120, repeats: true) { _ in
|
|
140
|
-
velocity *= 0.9875
|
|
141
|
-
animTranslation = (-velocity / 120) + animTranslation
|
|
142
|
-
|
|
143
|
-
let nextOffset = self.dampenOffset(animTranslation + self.initialOffset)
|
|
144
|
-
|
|
145
|
-
if nextOffset <= 0 {
|
|
146
|
-
if self.initialOffset <= 1 {
|
|
147
|
-
self.scrollToOffset(0)
|
|
148
|
-
} else {
|
|
149
|
-
sv.contentOffset.y = 0
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
self.stopTimer()
|
|
153
|
-
return
|
|
154
|
-
} else {
|
|
155
|
-
sv.contentOffset.y = nextOffset
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if abs(velocity) < 5 {
|
|
159
|
-
self.stopTimer()
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
func dampenOffset(_ offset: CGFloat) -> CGFloat {
|
|
165
|
-
if offset < 0 {
|
|
166
|
-
return offset - (offset * 0.55)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return offset
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
func tryFindScrollView() {
|
|
173
|
-
guard let scrollViewTag = scrollViewTag else {
|
|
174
|
-
return
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Before we switch to a different scrollview, we always want to remove the cancel gesture recognizer.
|
|
178
|
-
// Otherwise we might end up with duplicates when we switch back to that scrollview.
|
|
179
|
-
self.removeCancelGestureRecognizers()
|
|
180
|
-
|
|
181
|
-
self.rctScrollView = self.appContext?
|
|
182
|
-
.findView(withTag: scrollViewTag, ofType: RCTScrollView.self)
|
|
183
|
-
self.rctRefreshCtrl = self.rctScrollView?.scrollView.refreshControl as? RCTRefreshControl
|
|
184
|
-
|
|
185
|
-
self.addCancelGestureRecognizers()
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
func addCancelGestureRecognizers() {
|
|
189
|
-
self.cancelGestureRecognizers?.forEach { r in
|
|
190
|
-
self.rctScrollView?.scrollView?.addGestureRecognizer(r)
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
func removeCancelGestureRecognizers() {
|
|
195
|
-
self.cancelGestureRecognizers?.forEach { r in
|
|
196
|
-
self.rctScrollView?.scrollView?.removeGestureRecognizer(r)
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
func enableCancelGestureRecognizers() {
|
|
201
|
-
self.cancelGestureRecognizers?.forEach { r in
|
|
202
|
-
r.isEnabled = true
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
func disableCancelGestureRecognizers() {
|
|
207
|
-
self.cancelGestureRecognizers?.forEach { r in
|
|
208
|
-
r.isEnabled = false
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
func scrollToOffset(_ offset: Int, animated: Bool = true) {
|
|
213
|
-
self.rctScrollView?.scroll(toOffset: CGPoint(x: 0, y: offset), animated: animated)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
func stopTimer() {
|
|
217
|
-
self.disableCancelGestureRecognizers()
|
|
218
|
-
self.animTimer?.invalidate()
|
|
219
|
-
self.animTimer = nil
|
|
220
|
-
}
|
|
221
|
-
}
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
|
|
4
|
+
var scrollViewTag: Int? {
|
|
5
|
+
didSet {
|
|
6
|
+
self.tryFindScrollView()
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
private var rctScrollView: RCTScrollView?
|
|
11
|
+
private var rctRefreshCtrl: RCTRefreshControl?
|
|
12
|
+
private var cancelGestureRecognizers: [UIGestureRecognizer]?
|
|
13
|
+
private var animTimer: Timer?
|
|
14
|
+
private var initialOffset: CGFloat = 0.0
|
|
15
|
+
private var didImpact: Bool = false
|
|
16
|
+
|
|
17
|
+
required init(appContext: AppContext? = nil) {
|
|
18
|
+
super.init(appContext: appContext)
|
|
19
|
+
|
|
20
|
+
let pg = UIPanGestureRecognizer(target: self, action: #selector(callOnPan(_:)))
|
|
21
|
+
pg.delegate = self
|
|
22
|
+
self.addGestureRecognizer(pg)
|
|
23
|
+
|
|
24
|
+
let tg = UITapGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
|
|
25
|
+
tg.isEnabled = false
|
|
26
|
+
tg.delegate = self
|
|
27
|
+
|
|
28
|
+
let lpg = UILongPressGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
|
|
29
|
+
lpg.minimumPressDuration = 0.01
|
|
30
|
+
lpg.isEnabled = false
|
|
31
|
+
lpg.delegate = self
|
|
32
|
+
|
|
33
|
+
self.cancelGestureRecognizers = [lpg, tg]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
37
|
+
if gestureRecognizer is UIPanGestureRecognizer, otherGestureRecognizer is UIPanGestureRecognizer {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
44
|
+
guard let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else {
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
let velocity = gestureRecognizer.velocity(in: self)
|
|
48
|
+
return abs(velocity.y) > abs(velocity.x)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
52
|
+
self.stopTimer()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@IBAction func callOnPress(_ sender: UITapGestureRecognizer) {
|
|
56
|
+
self.stopTimer()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@IBAction func callOnPan(_ sender: UIPanGestureRecognizer) {
|
|
60
|
+
guard let rctsv = self.rctScrollView, let sv = rctsv.scrollView else { return }
|
|
61
|
+
|
|
62
|
+
let translation = sender.translation(in: self).y
|
|
63
|
+
|
|
64
|
+
if sender.state == .began {
|
|
65
|
+
if sv.contentOffset.y < 0 { sv.contentOffset.y = 0 }
|
|
66
|
+
self.initialOffset = sv.contentOffset.y
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if sender.state == .changed {
|
|
70
|
+
sv.contentOffset.y = self.dampenOffset(-translation + self.initialOffset)
|
|
71
|
+
if sv.contentOffset.y <= -130, !didImpact {
|
|
72
|
+
let generator = UIImpactFeedbackGenerator(style: .light)
|
|
73
|
+
generator.impactOccurred()
|
|
74
|
+
self.didImpact = true
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if sender.state == .ended {
|
|
79
|
+
let velocity = sender.velocity(in: self).y
|
|
80
|
+
self.didImpact = false
|
|
81
|
+
|
|
82
|
+
if sv.contentOffset.y <= -130 {
|
|
83
|
+
self.rctRefreshCtrl?.forwarderBeginRefreshing()
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if abs(velocity) < 250, sv.contentOffset.y >= 0 { return }
|
|
88
|
+
self.startDecayAnimation(translation, velocity)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func startDecayAnimation(_ translation: CGFloat, _ velocity: CGFloat) {
|
|
93
|
+
guard let sv = self.rctScrollView?.scrollView else { return }
|
|
94
|
+
|
|
95
|
+
var velocity = velocity
|
|
96
|
+
self.enableCancelGestureRecognizers()
|
|
97
|
+
|
|
98
|
+
if velocity > 0 { velocity = min(velocity, 5000) }
|
|
99
|
+
else { velocity = max(velocity, -5000) }
|
|
100
|
+
|
|
101
|
+
var animTranslation = -translation
|
|
102
|
+
self.animTimer = Timer.scheduledTimer(withTimeInterval: 1.0/120, repeats: true) { _ in
|
|
103
|
+
velocity *= 0.9875
|
|
104
|
+
animTranslation = (-velocity/120) + animTranslation
|
|
105
|
+
let nextOffset = self.dampenOffset(animTranslation + self.initialOffset)
|
|
106
|
+
|
|
107
|
+
if nextOffset <= 0 {
|
|
108
|
+
if self.initialOffset <= 1 { self.scrollToOffset(0) }
|
|
109
|
+
else { sv.contentOffset.y = 0 }
|
|
110
|
+
self.stopTimer()
|
|
111
|
+
return
|
|
112
|
+
} else {
|
|
113
|
+
sv.contentOffset.y = nextOffset
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if abs(velocity) < 5 { self.stopTimer() }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func dampenOffset(_ offset: CGFloat) -> CGFloat {
|
|
121
|
+
return offset < 0 ? offset - (offset*0.55) : offset
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func tryFindScrollView() {
|
|
125
|
+
guard let scrollViewTag = scrollViewTag else { return }
|
|
126
|
+
self.removeCancelGestureRecognizers()
|
|
127
|
+
self.rctScrollView = self.appContext?.findView(withTag: scrollViewTag, ofType: RCTScrollView.self)
|
|
128
|
+
self.rctRefreshCtrl = self.rctScrollView?.scrollView.refreshControl as? RCTRefreshControl
|
|
129
|
+
self.addCancelGestureRecognizers()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func addCancelGestureRecognizers() { self.cancelGestureRecognizers?.forEach { r in self.rctScrollView?.scrollView?.addGestureRecognizer(r) } }
|
|
133
|
+
func removeCancelGestureRecognizers() { self.cancelGestureRecognizers?.forEach { r in self.rctScrollView?.scrollView?.removeGestureRecognizer(r) } }
|
|
134
|
+
func enableCancelGestureRecognizers() { self.cancelGestureRecognizers?.forEach { r in r.isEnabled = true } }
|
|
135
|
+
func disableCancelGestureRecognizers() { self.cancelGestureRecognizers?.forEach { r in r.isEnabled = false } }
|
|
136
|
+
func scrollToOffset(_ offset: Int, animated: Bool = true) { self.rctScrollView?.scroll(toOffset: CGPoint(x:0,y:offset), animated:animated) }
|
|
137
|
+
func stopTimer() { self.disableCancelGestureRecognizers(); self.animTimer?.invalidate(); self.animTimer=nil }
|
|
138
|
+
}
|
package/.eslintrc.js
DELETED
package/android/build.gradle
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
apply plugin: 'com.android.library'
|
|
2
|
-
|
|
3
|
-
group = 'expo.modules.scrollforwarder'
|
|
4
|
-
version = '0.1.0'
|
|
5
|
-
|
|
6
|
-
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
|
-
apply from: expoModulesCorePlugin
|
|
8
|
-
applyKotlinExpoModulesCorePlugin()
|
|
9
|
-
useCoreDependencies()
|
|
10
|
-
useExpoPublishing()
|
|
11
|
-
|
|
12
|
-
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
|
13
|
-
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
|
14
|
-
// Most of the time, you may like to manage the Android SDK versions yourself.
|
|
15
|
-
def useManagedAndroidSdkVersions = false
|
|
16
|
-
if (useManagedAndroidSdkVersions) {
|
|
17
|
-
useDefaultAndroidSdkVersions()
|
|
18
|
-
} else {
|
|
19
|
-
buildscript {
|
|
20
|
-
// Simple helper that allows the root project to override versions declared by this library.
|
|
21
|
-
ext.safeExtGet = { prop, fallback ->
|
|
22
|
-
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
project.android {
|
|
26
|
-
compileSdkVersion safeExtGet("compileSdkVersion", 36)
|
|
27
|
-
defaultConfig {
|
|
28
|
-
minSdkVersion safeExtGet("minSdkVersion", 24)
|
|
29
|
-
targetSdkVersion safeExtGet("targetSdkVersion", 36)
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
android {
|
|
35
|
-
namespace "expo.modules.scrollforwarder"
|
|
36
|
-
defaultConfig {
|
|
37
|
-
versionCode 1
|
|
38
|
-
versionName "0.1.0"
|
|
39
|
-
}
|
|
40
|
-
lintOptions {
|
|
41
|
-
abortOnError false
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Add the dependencies block here
|
|
46
|
-
dependencies {
|
|
47
|
-
implementation 'com.facebook.react:react-android'
|
|
48
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
package expo.modules.scrollforwarder
|
|
2
|
-
|
|
3
|
-
import expo.modules.kotlin.modules.Module
|
|
4
|
-
import expo.modules.kotlin.modules.ModuleDefinition
|
|
5
|
-
import expo.modules.kotlin.views.ExpoView
|
|
6
|
-
|
|
7
|
-
class ExpoScrollForwarderModule : Module() {
|
|
8
|
-
override fun definition() = ModuleDefinition {
|
|
9
|
-
Name("ExpoScrollForwarder")
|
|
10
|
-
|
|
11
|
-
View(ExpoScrollForwarderView::class) {
|
|
12
|
-
Prop("scrollViewTag") { view: ExpoScrollForwarderView, prop: Int ->
|
|
13
|
-
view.scrollViewTag = prop
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
package expo.modules.scrollforwarder
|
|
2
|
-
|
|
3
|
-
import android.animation.ValueAnimator
|
|
4
|
-
import android.content.Context
|
|
5
|
-
import android.view.GestureDetector
|
|
6
|
-
import android.view.MotionEvent
|
|
7
|
-
import android.view.animation.DecelerateInterpolator
|
|
8
|
-
import androidx.core.view.GestureDetectorCompat
|
|
9
|
-
import com.facebook.react.views.scroll.ReactScrollView
|
|
10
|
-
import expo.modules.kotlin.AppContext
|
|
11
|
-
import expo.modules.kotlin.views.ExpoView
|
|
12
|
-
import kotlin.math.abs
|
|
13
|
-
|
|
14
|
-
class ExpoScrollForwarderView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
|
15
|
-
|
|
16
|
-
var scrollViewTag: Int? = null
|
|
17
|
-
set(value) {
|
|
18
|
-
field = value
|
|
19
|
-
tryFindScrollView()
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
private var reactScrollView: ReactScrollView? = null
|
|
23
|
-
private var gestureDetector: GestureDetectorCompat
|
|
24
|
-
private var isScrolling = false
|
|
25
|
-
private var initialScrollY = 0
|
|
26
|
-
private var lastY = 0f
|
|
27
|
-
private var decayAnimator: ValueAnimator? = null
|
|
28
|
-
private var didImpact = false
|
|
29
|
-
|
|
30
|
-
init {
|
|
31
|
-
gestureDetector = GestureDetectorCompat(context, GestureListener())
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
|
35
|
-
val scrollView = reactScrollView ?: return super.onInterceptTouchEvent(ev)
|
|
36
|
-
|
|
37
|
-
when (ev.action) {
|
|
38
|
-
MotionEvent.ACTION_DOWN -> {
|
|
39
|
-
stopDecayAnimation()
|
|
40
|
-
lastY = ev.rawY
|
|
41
|
-
initialScrollY = scrollView.scrollY
|
|
42
|
-
isScrolling = false
|
|
43
|
-
}
|
|
44
|
-
MotionEvent.ACTION_MOVE -> {
|
|
45
|
-
val deltaY = lastY - ev.rawY
|
|
46
|
-
|
|
47
|
-
// Check if this is a vertical scroll gesture
|
|
48
|
-
if (!isScrolling && abs(deltaY) > 10) {
|
|
49
|
-
// Calculate if the gesture is more vertical than horizontal
|
|
50
|
-
val initialX = if (ev.historySize > 0) ev.getHistoricalX(0) else ev.x
|
|
51
|
-
val initialY = if (ev.historySize > 0) ev.getHistoricalY(0) else ev.y
|
|
52
|
-
val deltaX = ev.x - initialX
|
|
53
|
-
val deltaYCheck = ev.y - initialY
|
|
54
|
-
|
|
55
|
-
if (abs(deltaYCheck) > abs(deltaX)) {
|
|
56
|
-
isScrolling = true
|
|
57
|
-
return true
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return isScrolling || super.onInterceptTouchEvent(ev)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
67
|
-
val scrollView = reactScrollView ?: return super.onTouchEvent(event)
|
|
68
|
-
|
|
69
|
-
gestureDetector.onTouchEvent(event)
|
|
70
|
-
|
|
71
|
-
when (event.action) {
|
|
72
|
-
MotionEvent.ACTION_DOWN -> {
|
|
73
|
-
stopDecayAnimation()
|
|
74
|
-
lastY = event.rawY
|
|
75
|
-
initialScrollY = scrollView.scrollY
|
|
76
|
-
if (scrollView.scrollY < 0) {
|
|
77
|
-
scrollView.scrollTo(0, 0)
|
|
78
|
-
}
|
|
79
|
-
return true
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
MotionEvent.ACTION_MOVE -> {
|
|
83
|
-
val deltaY = (lastY - event.rawY).toInt()
|
|
84
|
-
val newOffset = dampenOffset(initialScrollY + deltaY)
|
|
85
|
-
|
|
86
|
-
scrollView.scrollTo(0, newOffset)
|
|
87
|
-
|
|
88
|
-
// Haptic feedback at refresh threshold
|
|
89
|
-
if (newOffset <= -130 && !didImpact) {
|
|
90
|
-
performHapticFeedback(android.view.HapticFeedbackConstants.LONG_PRESS)
|
|
91
|
-
didImpact = true
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return true
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
98
|
-
val velocityY = calculateVelocity(event)
|
|
99
|
-
didImpact = false
|
|
100
|
-
|
|
101
|
-
// Check for pull-to-refresh threshold
|
|
102
|
-
if (scrollView.scrollY <= -130) {
|
|
103
|
-
triggerRefresh(scrollView)
|
|
104
|
-
return true
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Don't animate if velocity is too low and we're at a valid position
|
|
108
|
-
if (abs(velocityY) < 250 && scrollView.scrollY >= 0) {
|
|
109
|
-
isScrolling = false
|
|
110
|
-
return true
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
startDecayAnimation(scrollView, velocityY.toFloat())
|
|
114
|
-
isScrolling = false
|
|
115
|
-
return true
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return super.onTouchEvent(event)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private fun calculateVelocity(event: MotionEvent): Int {
|
|
123
|
-
if (event.historySize == 0) return 0
|
|
124
|
-
|
|
125
|
-
val lastHistoricalY = event.getHistoricalY(event.historySize - 1)
|
|
126
|
-
val lastHistoricalTime = event.getHistoricalEventTime(event.historySize - 1)
|
|
127
|
-
val deltaY = event.y - lastHistoricalY
|
|
128
|
-
val deltaTime = (event.eventTime - lastHistoricalTime) / 1000f
|
|
129
|
-
|
|
130
|
-
return if (deltaTime > 0) {
|
|
131
|
-
(-deltaY / deltaTime).toInt()
|
|
132
|
-
} else {
|
|
133
|
-
0
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
private fun startDecayAnimation(scrollView: ReactScrollView, velocity: Float) {
|
|
138
|
-
var currentVelocity = velocity.coerceIn(-5000f, 5000f)
|
|
139
|
-
val startOffset = scrollView.scrollY
|
|
140
|
-
var currentOffset = startOffset.toFloat()
|
|
141
|
-
|
|
142
|
-
decayAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
|
|
143
|
-
duration = 3000
|
|
144
|
-
interpolator = DecelerateInterpolator()
|
|
145
|
-
|
|
146
|
-
addUpdateListener { animator ->
|
|
147
|
-
currentVelocity *= 0.9875f
|
|
148
|
-
currentOffset += (-currentVelocity / 120f)
|
|
149
|
-
|
|
150
|
-
val newOffset = dampenOffset(currentOffset.toInt())
|
|
151
|
-
|
|
152
|
-
when {
|
|
153
|
-
newOffset <= 0 -> {
|
|
154
|
-
scrollView.smoothScrollTo(0, 0)
|
|
155
|
-
cancel()
|
|
156
|
-
}
|
|
157
|
-
else -> {
|
|
158
|
-
scrollView.scrollTo(0, newOffset)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (abs(currentVelocity) < 5) {
|
|
163
|
-
cancel()
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
decayAnimator?.start()
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
private fun dampenOffset(offset: Int): Int {
|
|
171
|
-
return if (offset < 0) {
|
|
172
|
-
(offset - (offset * 0.55)).toInt()
|
|
173
|
-
} else {
|
|
174
|
-
offset
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
private fun triggerRefresh(scrollView: ReactScrollView) {
|
|
179
|
-
// Trigger refresh by finding and activating the refresh control
|
|
180
|
-
try {
|
|
181
|
-
for (i in 0 until scrollView.childCount) {
|
|
182
|
-
val child = scrollView.getChildAt(i)
|
|
183
|
-
if (child.javaClass.simpleName.contains("Refresh")) {
|
|
184
|
-
// Try to invoke setRefreshing via reflection
|
|
185
|
-
val method = child.javaClass.getMethod("setRefreshing", Boolean::class.javaPrimitiveType)
|
|
186
|
-
method.invoke(child, true)
|
|
187
|
-
break
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
} catch (e: Exception) {
|
|
191
|
-
// Fallback: just scroll to trigger position
|
|
192
|
-
scrollView.scrollTo(0, -140)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
private fun stopDecayAnimation() {
|
|
197
|
-
decayAnimator?.cancel()
|
|
198
|
-
decayAnimator = null
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
private fun tryFindScrollView() {
|
|
202
|
-
val tag = scrollViewTag ?: return
|
|
203
|
-
|
|
204
|
-
reactScrollView = appContext?.findView(tag) as? ReactScrollView
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
|
|
208
|
-
override fun onDown(e: MotionEvent): Boolean {
|
|
209
|
-
stopDecayAnimation()
|
|
210
|
-
return true
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
|
214
|
-
stopDecayAnimation()
|
|
215
|
-
return true
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
override fun onDetachedFromWindow() {
|
|
220
|
-
super.onDetachedFromWindow()
|
|
221
|
-
stopDecayAnimation()
|
|
222
|
-
}
|
|
223
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoScrollForwarder.types.d.ts","sourceRoot":"","sources":["../src/ExpoScrollForwarder.types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,4BAA4B;IAC3C,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoScrollForwarder.types.js","sourceRoot":"","sources":["../src/ExpoScrollForwarder.types.ts"],"names":[],"mappings":"","sourcesContent":["export interface ExpoScrollForwarderViewProps {\n scrollViewTag: number | null\n children: React.ReactNode\n}\n"]}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import { ExpoScrollForwarderViewProps } from "./ExpoScrollForwarder.types";
|
|
3
|
-
export declare function ExpoScrollForwarderView({ children, ...rest }: ExpoScrollForwarderViewProps): React.JSX.Element;
|
|
4
|
-
//# sourceMappingURL=ExpoScrollForwarderView.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoScrollForwarderView.d.ts","sourceRoot":"","sources":["../src/ExpoScrollForwarderView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAC;AAK3E,wBAAgB,uBAAuB,CAAC,EACtC,QAAQ,EACR,GAAG,IAAI,EACR,EAAE,4BAA4B,qBAE9B"}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { requireNativeViewManager } from "expo-modules-core";
|
|
2
|
-
import * as React from "react";
|
|
3
|
-
const NativeView = requireNativeViewManager("ExpoScrollForwarder");
|
|
4
|
-
export function ExpoScrollForwarderView({ children, ...rest }) {
|
|
5
|
-
return <NativeView {...rest}>{children}</NativeView>;
|
|
6
|
-
}
|
|
7
|
-
//# sourceMappingURL=ExpoScrollForwarderView.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoScrollForwarderView.js","sourceRoot":"","sources":["../src/ExpoScrollForwarderView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,MAAM,UAAU,GACd,wBAAwB,CAAC,qBAAqB,CAAC,CAAC;AAElD,MAAM,UAAU,uBAAuB,CAAC,EACtC,QAAQ,EACR,GAAG,IAAI,EACsB;IAC7B,OAAO,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC,CAAC;AACvD,CAAC","sourcesContent":["import { requireNativeViewManager } from \"expo-modules-core\";\nimport * as React from \"react\";\nimport { ExpoScrollForwarderViewProps } from \"./ExpoScrollForwarder.types\";\n\nconst NativeView: React.ComponentType<ExpoScrollForwarderViewProps> =\n requireNativeViewManager(\"ExpoScrollForwarder\");\n\nexport function ExpoScrollForwarderView({\n children,\n ...rest\n}: ExpoScrollForwarderViewProps) {\n return <NativeView {...rest}>{children}</NativeView>;\n}\n"]}
|
package/build/index.d.ts
DELETED
package/build/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC"}
|
package/build/index.js
DELETED
package/build/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC","sourcesContent":["export { ExpoScrollForwarderView } from \"./ExpoScrollForwarderView\";\n"]}
|
|
@@ -1,29 +0,0 @@
|
|
|
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 = 'ExpoScrollForwarder'
|
|
7
|
-
s.version = package['version']
|
|
8
|
-
s.summary = package['description']
|
|
9
|
-
s.description = package['description']
|
|
10
|
-
s.license = package['license']
|
|
11
|
-
s.author = package['author']
|
|
12
|
-
s.homepage = package['homepage']
|
|
13
|
-
s.platforms = {
|
|
14
|
-
:ios => '15.1',
|
|
15
|
-
:tvos => '15.1'
|
|
16
|
-
}
|
|
17
|
-
s.swift_version = '5.9'
|
|
18
|
-
s.source = { git: 'https://github.com/sharifrayhan/expo-scroll-forwarder' }
|
|
19
|
-
s.static_framework = true
|
|
20
|
-
|
|
21
|
-
s.dependency 'ExpoModulesCore'
|
|
22
|
-
|
|
23
|
-
# Swift/Objective-C compatibility
|
|
24
|
-
s.pod_target_xcconfig = {
|
|
25
|
-
'DEFINES_MODULE' => 'YES',
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
29
|
-
end
|
package/src/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { ExpoScrollForwarderView } from "./ExpoScrollForwarderView";
|
package/tsconfig.json
DELETED