expo-scroll-forwarder 0.1.5 → 0.1.6

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.
@@ -41,3 +41,8 @@ android {
41
41
  abortOnError false
42
42
  }
43
43
  }
44
+
45
+ // Add the dependencies block here
46
+ dependencies {
47
+ implementation 'com.facebook.react:react-android'
48
+ }
@@ -1,19 +1,17 @@
1
1
  package expo.modules.scrollforwarder
2
2
 
3
- import com.facebook.react.views.scroll.ReactScrollView
4
- import com.facebook.react.views.scroll.ReactScrollViewManager
5
- import expo.modules.core.Module
6
- import expo.modules.core.ModuleDefinition
7
- import expo.modules.core.ViewManagerDefinition
3
+ import expo.modules.kotlin.modules.Module
4
+ import expo.modules.kotlin.modules.ModuleDefinition
5
+ import expo.modules.kotlin.views.ExpoView
8
6
 
9
7
  class ExpoScrollForwarderModule : Module() {
10
- override fun definition(): ModuleDefinition {
11
- Name("ExpoScrollForwarder")
8
+ override fun definition() = ModuleDefinition {
9
+ Name("ExpoScrollForwarder")
12
10
 
13
- View(ExpoScrollForwarderView::class.java) {
14
- Prop("scrollViewTag") { view: ExpoScrollForwarderView, prop: Int? ->
15
- view.scrollViewTag = prop
16
- }
17
- }
11
+ View(ExpoScrollForwarderView::class) {
12
+ Prop("scrollViewTag") { view: ExpoScrollForwarderView, prop: Int ->
13
+ view.scrollViewTag = prop
14
+ }
18
15
  }
19
- }
16
+ }
17
+ }
@@ -1,38 +1,223 @@
1
1
  package expo.modules.scrollforwarder
2
2
 
3
+ import android.animation.ValueAnimator
3
4
  import android.content.Context
4
- import android.view.View
5
- import android.widget.ScrollView
6
- import androidx.core.widget.NestedScrollView
7
- import expo.modules.core.views.ExpoView
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
8
13
 
9
- class ExpoScrollForwarderView(context: Context) : ExpoView(context) {
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
+ }
10
33
 
11
- var scrollViewTag: Int? = null
12
- set(value) {
13
- field = value
14
- tryFindScrollView()
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
+ }
15
59
  }
60
+ }
61
+ }
62
+
63
+ return isScrolling || super.onInterceptTouchEvent(ev)
64
+ }
16
65
 
17
- private var scrollView: NestedScrollView? = null
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
+ }
18
136
 
19
- private fun tryFindScrollView() {
20
- scrollViewTag?.let { tag ->
21
- val parentView = rootView
22
- val found = parentView.findViewById<View>(tag)
23
- if (found is NestedScrollView) {
24
- scrollView = found
25
- }
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
+ }
26
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
27
175
  }
176
+ }
28
177
 
29
- fun scrollToOffset(offset: Int, animated: Boolean = true) {
30
- scrollView?.let {
31
- if (animated) {
32
- it.smoothScrollTo(0, offset)
33
- } else {
34
- it.scrollTo(0, offset)
35
- }
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
36
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
37
211
  }
38
- }
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,3 +1,4 @@
1
- import { type ExpoScrollForwarderViewProps } from './ExpoScrollForwarder.types';
2
- export declare function ExpoScrollForwarderView({ children, }: React.PropsWithChildren<ExpoScrollForwarderViewProps>): import("react").ReactNode;
1
+ import * as React from "react";
2
+ import { ExpoScrollForwarderViewProps } from "./ExpoScrollForwarder.types";
3
+ export declare function ExpoScrollForwarderView({ children, ...rest }: ExpoScrollForwarderViewProps): React.JSX.Element;
3
4
  //# sourceMappingURL=ExpoScrollForwarderView.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoScrollForwarderView.d.ts","sourceRoot":"","sources":["../src/ExpoScrollForwarderView.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAC,KAAK,4BAA4B,EAAC,MAAM,6BAA6B,CAAA;AAE7E,wBAAgB,uBAAuB,CAAC,EACtC,QAAQ,GACT,EAAE,KAAK,CAAC,iBAAiB,CAAC,4BAA4B,CAAC,6BAEvD"}
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,4 +1,7 @@
1
- export function ExpoScrollForwarderView({ children, }) {
2
- return children;
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>;
3
6
  }
4
7
  //# sourceMappingURL=ExpoScrollForwarderView.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoScrollForwarderView.js","sourceRoot":"","sources":["../src/ExpoScrollForwarderView.tsx"],"names":[],"mappings":"AAEA,MAAM,UAAU,uBAAuB,CAAC,EACtC,QAAQ,GAC8C;IACtD,OAAO,QAAQ,CAAA;AACjB,CAAC","sourcesContent":["import {type ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types'\n\nexport function ExpoScrollForwarderView({\n children,\n}: React.PropsWithChildren<ExpoScrollForwarderViewProps>) {\n return children\n}\n"]}
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-scroll-forwarder",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Forward scroll gestures from any view to a ScrollView in React Native and Expo.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -1,7 +1,13 @@
1
- import {type ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types'
1
+ import { requireNativeViewManager } from "expo-modules-core";
2
+ import * as React from "react";
3
+ import { ExpoScrollForwarderViewProps } from "./ExpoScrollForwarder.types";
4
+
5
+ const NativeView: React.ComponentType<ExpoScrollForwarderViewProps> =
6
+ requireNativeViewManager("ExpoScrollForwarder");
2
7
 
3
8
  export function ExpoScrollForwarderView({
4
9
  children,
5
- }: React.PropsWithChildren<ExpoScrollForwarderViewProps>) {
6
- return children
10
+ ...rest
11
+ }: ExpoScrollForwarderViewProps) {
12
+ return <NativeView {...rest}>{children}</NativeView>;
7
13
  }
@@ -1,3 +0,0 @@
1
- import { ExpoScrollForwarderViewProps } from "./ExpoScrollForwarder.types";
2
- export declare function ExpoScrollForwarderView({ children, ...rest }: ExpoScrollForwarderViewProps): import("react").JSX.Element;
3
- //# sourceMappingURL=ExpoScrollForwarderView.android.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ExpoScrollForwarderView.android.d.ts","sourceRoot":"","sources":["../src/ExpoScrollForwarderView.android.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAC;AAK3E,wBAAgB,uBAAuB,CAAC,EACtC,QAAQ,EACR,GAAG,IAAI,EACR,EAAE,4BAA4B,+BAE9B"}
@@ -1,6 +0,0 @@
1
- import { requireNativeViewManager } from "expo-modules-core";
2
- const NativeView = requireNativeViewManager("ExpoScrollForwarder");
3
- export function ExpoScrollForwarderView({ children, ...rest }) {
4
- return <NativeView {...rest}>{children}</NativeView>;
5
- }
6
- //# sourceMappingURL=ExpoScrollForwarderView.android.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ExpoScrollForwarderView.android.js","sourceRoot":"","sources":["../src/ExpoScrollForwarderView.android.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAG7D,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\";\r\nimport { ExpoScrollForwarderViewProps } from \"./ExpoScrollForwarder.types\";\r\n\r\nconst NativeView: React.ComponentType<ExpoScrollForwarderViewProps> =\r\n requireNativeViewManager(\"ExpoScrollForwarder\");\r\n\r\nexport function ExpoScrollForwarderView({\r\n children,\r\n ...rest\r\n}: ExpoScrollForwarderViewProps) {\r\n return <NativeView {...rest}>{children}</NativeView>;\r\n}\r\n"]}
@@ -1,3 +0,0 @@
1
- import { type ExpoScrollForwarderViewProps } from './ExpoScrollForwarder.types';
2
- export declare function ExpoScrollForwarderView({ children, ...rest }: ExpoScrollForwarderViewProps): import("react").JSX.Element;
3
- //# sourceMappingURL=ExpoScrollForwarderView.ios.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ExpoScrollForwarderView.ios.d.ts","sourceRoot":"","sources":["../src/ExpoScrollForwarderView.ios.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAC,KAAK,4BAA4B,EAAC,MAAM,6BAA6B,CAAA;AAK7E,wBAAgB,uBAAuB,CAAC,EACtC,QAAQ,EACR,GAAG,IAAI,EACR,EAAE,4BAA4B,+BAE9B"}
@@ -1,6 +0,0 @@
1
- import { requireNativeViewManager } from 'expo-modules-core';
2
- const NativeView = requireNativeViewManager('ExpoScrollForwarder');
3
- export function ExpoScrollForwarderView({ children, ...rest }) {
4
- return <NativeView {...rest}>{children}</NativeView>;
5
- }
6
- //# sourceMappingURL=ExpoScrollForwarderView.ios.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ExpoScrollForwarderView.ios.js","sourceRoot":"","sources":["../src/ExpoScrollForwarderView.ios.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAC,wBAAwB,EAAC,MAAM,mBAAmB,CAAA;AAI1D,MAAM,UAAU,GACd,wBAAwB,CAAC,qBAAqB,CAAC,CAAA;AAEjD,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,CAAA;AACtD,CAAC","sourcesContent":["import {requireNativeViewManager} from 'expo-modules-core'\n\nimport {type 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"]}
@@ -1,12 +0,0 @@
1
- import { requireNativeViewManager } from "expo-modules-core";
2
- import { ExpoScrollForwarderViewProps } from "./ExpoScrollForwarder.types";
3
-
4
- const NativeView: React.ComponentType<ExpoScrollForwarderViewProps> =
5
- requireNativeViewManager("ExpoScrollForwarder");
6
-
7
- export function ExpoScrollForwarderView({
8
- children,
9
- ...rest
10
- }: ExpoScrollForwarderViewProps) {
11
- return <NativeView {...rest}>{children}</NativeView>;
12
- }
@@ -1,13 +0,0 @@
1
- import {requireNativeViewManager} from 'expo-modules-core'
2
-
3
- import {type 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
- }