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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
43
|
+
|
|
44
|
+
return true
|
|
19
45
|
}
|
|
20
46
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
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