expo-scroll-forwarder 0.1.0 → 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.
@@ -1,48 +1,13 @@
1
1
  import ExpoModulesCore
2
2
 
3
3
  public class ExpoScrollForwarderModule: Module {
4
- // Each module class must implement the definition function. The definition consists of components
5
- // that describes the module's functionality and behavior.
6
- // See https://docs.expo.dev/modules/module-api for more details about available components.
7
4
  public func definition() -> ModuleDefinition {
8
- // Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
9
- // Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
10
- // The module will be accessible from `requireNativeModule('ExpoScrollForwarder')` in JavaScript.
11
5
  Name("ExpoScrollForwarder")
12
6
 
13
- // Defines constant property on the module.
14
- Constant("PI") {
15
- Double.pi
16
- }
17
-
18
- // Defines event names that the module can send to JavaScript.
19
- Events("onChange")
20
-
21
- // Defines a JavaScript synchronous function that runs the native code on the JavaScript thread.
22
- Function("hello") {
23
- return "Hello world! 👋"
24
- }
25
-
26
- // Defines a JavaScript function that always returns a Promise and whose native code
27
- // is by default dispatched on the different thread than the JavaScript runtime runs on.
28
- AsyncFunction("setValueAsync") { (value: String) in
29
- // Send an event to JavaScript.
30
- self.sendEvent("onChange", [
31
- "value": value
32
- ])
33
- }
34
-
35
- // Enables the module to be used as a native view. Definition components that are accepted as part of the
36
- // view definition: Prop, Events.
37
7
  View(ExpoScrollForwarderView.self) {
38
- // Defines a setter for the `url` prop.
39
- Prop("url") { (view: ExpoScrollForwarderView, url: URL) in
40
- if view.webView.url != url {
41
- view.webView.load(URLRequest(url: url))
42
- }
8
+ Prop("scrollViewTag") { (view: ExpoScrollForwarderView, prop: Int) in
9
+ view.scrollViewTag = prop
43
10
  }
44
-
45
- Events("onLoad")
46
11
  }
47
12
  }
48
13
  }
@@ -1,38 +1,213 @@
1
1
  import ExpoModulesCore
2
- import WebKit
3
2
 
4
3
  // This view will be used as a native component. Make sure to inherit from `ExpoView`
5
4
  // to apply the proper styling (e.g. border radius and shadows).
6
- class ExpoScrollForwarderView: ExpoView {
7
- let webView = WKWebView()
8
- let onLoad = EventDispatcher()
9
- var delegate: WebViewDelegate?
5
+ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
6
+ var scrollViewTag: Int? {
7
+ didSet {
8
+ self.tryFindScrollView()
9
+ }
10
+ }
11
+
12
+ private var rctScrollView: RCTScrollView?
13
+ private var rctRefreshCtrl: RCTRefreshControl?
14
+ private var cancelGestureRecognizers: [UIGestureRecognizer]?
15
+ private var animTimer: Timer?
16
+ private var initialOffset: CGFloat = 0.0
17
+ private var didImpact: Bool = false
10
18
 
11
19
  required init(appContext: AppContext? = nil) {
12
20
  super.init(appContext: appContext)
13
- clipsToBounds = true
14
- delegate = WebViewDelegate { url in
15
- self.onLoad(["url": url])
21
+
22
+ let pg = UIPanGestureRecognizer(target: self, action: #selector(callOnPan(_:)))
23
+ pg.delegate = self
24
+ self.addGestureRecognizer(pg)
25
+
26
+ let tg = UITapGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
27
+ tg.isEnabled = false
28
+ tg.delegate = self
29
+
30
+ let lpg = UILongPressGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
31
+ lpg.minimumPressDuration = 0.01
32
+ lpg.isEnabled = false
33
+ lpg.delegate = self
34
+
35
+ self.cancelGestureRecognizers = [lpg, tg]
36
+ }
37
+
38
+ // We don't want to recognize the scroll pan gesture and the swipe back gesture together
39
+ func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
40
+ if gestureRecognizer is UIPanGestureRecognizer, otherGestureRecognizer is UIPanGestureRecognizer {
41
+ return false
16
42
  }
17
- webView.navigationDelegate = delegate
18
- addSubview(webView)
43
+
44
+ return true
19
45
  }
20
46
 
21
- override func layoutSubviews() {
22
- webView.frame = bounds
47
+ // We only want the "scroll" gesture to happen whenever the pan is vertical, otherwise it will
48
+ // interfere with the native swipe back gesture.
49
+ override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
50
+ guard let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else {
51
+ return true
52
+ }
53
+
54
+ let velocity = gestureRecognizer.velocity(in: self)
55
+ return abs(velocity.y) > abs(velocity.x)
56
+ }
57
+
58
+ // This will be used to cancel the scroll animation whenever we tap inside of the header. We don't need another
59
+ // recognizer for this one.
60
+ override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
61
+ self.stopTimer()
62
+ }
63
+
64
+ // This will be used to cancel the animation whenever we press inside of the scroll view. We don't want to change
65
+ // the scroll view gesture's delegate, so we add an additional recognizer to detect this.
66
+ @IBAction func callOnPress(_ sender: UITapGestureRecognizer) {
67
+ self.stopTimer()
68
+ }
69
+
70
+ @IBAction func callOnPan(_ sender: UIPanGestureRecognizer) {
71
+ guard let rctsv = self.rctScrollView, let sv = rctsv.scrollView else {
72
+ return
73
+ }
74
+
75
+ let translation = sender.translation(in: self).y
76
+
77
+ if sender.state == .began {
78
+ if sv.contentOffset.y < 0 {
79
+ sv.contentOffset.y = 0
80
+ }
81
+
82
+ self.initialOffset = sv.contentOffset.y
83
+ }
84
+
85
+ if sender.state == .changed {
86
+ sv.contentOffset.y = self.dampenOffset(-translation + self.initialOffset)
87
+
88
+ if sv.contentOffset.y <= -130, !didImpact {
89
+ let generator = UIImpactFeedbackGenerator(style: .light)
90
+ generator.impactOccurred()
91
+
92
+ self.didImpact = true
93
+ }
94
+ }
95
+
96
+ if sender.state == .ended {
97
+ let velocity = sender.velocity(in: self).y
98
+ self.didImpact = false
99
+
100
+ if sv.contentOffset.y <= -130 {
101
+ self.rctRefreshCtrl?.forwarderBeginRefreshing()
102
+ return
103
+ }
104
+
105
+ // A check for a velocity under 250 prevents animations from occurring when they wouldn't in a normal
106
+ // scroll view
107
+ if abs(velocity) < 250, sv.contentOffset.y >= 0 {
108
+ return
109
+ }
110
+
111
+ self.startDecayAnimation(translation, velocity)
112
+ }
23
113
  }
24
- }
25
114
 
26
- class WebViewDelegate: NSObject, WKNavigationDelegate {
27
- let onUrlChange: (String) -> Void
115
+ func startDecayAnimation(_ translation: CGFloat, _ velocity: CGFloat) {
116
+ guard let sv = self.rctScrollView?.scrollView else {
117
+ return
118
+ }
119
+
120
+ var velocity = velocity
121
+
122
+ self.enableCancelGestureRecognizers()
123
+
124
+ if velocity > 0 {
125
+ velocity = min(velocity, 5000)
126
+ } else {
127
+ velocity = max(velocity, -5000)
128
+ }
129
+
130
+ var animTranslation = -translation
131
+ self.animTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 120, repeats: true) { _ in
132
+ velocity *= 0.9875
133
+ animTranslation = (-velocity / 120) + animTranslation
134
+
135
+ let nextOffset = self.dampenOffset(animTranslation + self.initialOffset)
136
+
137
+ if nextOffset <= 0 {
138
+ if self.initialOffset <= 1 {
139
+ self.scrollToOffset(0)
140
+ } else {
141
+ sv.contentOffset.y = 0
142
+ }
143
+
144
+ self.stopTimer()
145
+ return
146
+ } else {
147
+ sv.contentOffset.y = nextOffset
148
+ }
28
149
 
29
- init(onUrlChange: @escaping (String) -> Void) {
30
- self.onUrlChange = onUrlChange
150
+ if abs(velocity) < 5 {
151
+ self.stopTimer()
152
+ }
153
+ }
154
+ }
155
+
156
+ func dampenOffset(_ offset: CGFloat) -> CGFloat {
157
+ if offset < 0 {
158
+ return offset - (offset * 0.55)
159
+ }
160
+
161
+ return offset
31
162
  }
32
163
 
33
- func webView(_ webView: WKWebView, didFinish navigation: WKNavigation) {
34
- if let url = webView.url {
35
- onUrlChange(url.absoluteString)
164
+ func tryFindScrollView() {
165
+ guard let scrollViewTag = scrollViewTag else {
166
+ return
36
167
  }
168
+
169
+ // Before we switch to a different scrollview, we always want to remove the cancel gesture recognizer.
170
+ // Otherwise we might end up with duplicates when we switch back to that scrollview.
171
+ self.removeCancelGestureRecognizers()
172
+
173
+ self.rctScrollView = self.appContext?
174
+ .findView(withTag: scrollViewTag, ofType: RCTScrollView.self)
175
+ self.rctRefreshCtrl = self.rctScrollView?.scrollView.refreshControl as? RCTRefreshControl
176
+
177
+ self.addCancelGestureRecognizers()
178
+ }
179
+
180
+ func addCancelGestureRecognizers() {
181
+ self.cancelGestureRecognizers?.forEach { r in
182
+ self.rctScrollView?.scrollView?.addGestureRecognizer(r)
183
+ }
184
+ }
185
+
186
+ func removeCancelGestureRecognizers() {
187
+ self.cancelGestureRecognizers?.forEach { r in
188
+ self.rctScrollView?.scrollView?.removeGestureRecognizer(r)
189
+ }
190
+ }
191
+
192
+ func enableCancelGestureRecognizers() {
193
+ self.cancelGestureRecognizers?.forEach { r in
194
+ r.isEnabled = true
195
+ }
196
+ }
197
+
198
+ func disableCancelGestureRecognizers() {
199
+ self.cancelGestureRecognizers?.forEach { r in
200
+ r.isEnabled = false
201
+ }
202
+ }
203
+
204
+ func scrollToOffset(_ offset: Int, animated: Bool = true) {
205
+ self.rctScrollView?.scroll(toOffset: CGPoint(x: 0, y: offset), animated: animated)
206
+ }
207
+
208
+ func stopTimer() {
209
+ self.disableCancelGestureRecognizers()
210
+ self.animTimer?.invalidate()
211
+ self.animTimer = nil
37
212
  }
38
213
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-scroll-forwarder",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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",