forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import ExpoModulesCore
2import React
3
4// This view will be used as a native component. Make sure to inherit from `ExpoView`
5// to apply the proper styling (e.g. border radius and shadows).
6class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
7 var scrollViewTag: Int? {
8 didSet {
9 self.tryFindScrollView()
10 }
11 }
12
13 private var rctScrollView: RCTScrollView?
14 private var rctRefreshCtrl: RCTRefreshControl?
15 private var cancelGestureRecognizers: [UIGestureRecognizer]?
16 private var animTimer: Timer?
17 private var initialOffset: CGFloat = 0.0
18 private var didImpact: Bool = false
19
20 required init(appContext: AppContext? = nil) {
21 super.init(appContext: appContext)
22
23 let pg = UIPanGestureRecognizer(target: self, action: #selector(callOnPan(_:)))
24 pg.delegate = self
25 self.addGestureRecognizer(pg)
26
27 let tg = UITapGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
28 tg.isEnabled = false
29 tg.delegate = self
30
31 let lpg = UILongPressGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
32 lpg.minimumPressDuration = 0.01
33 lpg.isEnabled = false
34 lpg.delegate = self
35
36 self.cancelGestureRecognizers = [lpg, tg]
37 }
38
39 // We don't want to recognize the scroll pan gesture and the swipe back gesture together
40 func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
41 if gestureRecognizer is UIPanGestureRecognizer, otherGestureRecognizer is UIPanGestureRecognizer {
42 return false
43 }
44
45 return true
46 }
47
48 // We only want the "scroll" gesture to happen whenever the pan is vertical, otherwise it will
49 // interfere with the native swipe back gesture.
50 override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
51 guard let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else {
52 return true
53 }
54
55 let velocity = gestureRecognizer.velocity(in: self)
56 return abs(velocity.y) > abs(velocity.x)
57 }
58
59 // This will be used to cancel the scroll animation whenever we tap inside of the header. We don't need another
60 // recognizer for this one.
61 override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
62 self.stopTimer()
63 }
64
65 // This will be used to cancel the animation whenever we press inside of the scroll view. We don't want to change
66 // the scroll view gesture's delegate, so we add an additional recognizer to detect this.
67 @IBAction func callOnPress(_ sender: UITapGestureRecognizer) {
68 self.stopTimer()
69 }
70
71 @IBAction func callOnPan(_ sender: UIPanGestureRecognizer) {
72 guard let rctsv = self.rctScrollView, let sv = rctsv.scrollView else {
73 return
74 }
75
76 let translation = sender.translation(in: self).y
77
78 if sender.state == .began {
79 if sv.contentOffset.y < 0 {
80 sv.contentOffset.y = 0
81 }
82
83 self.initialOffset = sv.contentOffset.y
84 }
85
86 if sender.state == .changed {
87 sv.contentOffset.y = self.dampenOffset(-translation + self.initialOffset)
88
89 if sv.contentOffset.y <= -130, !didImpact {
90 let generator = UIImpactFeedbackGenerator(style: .light)
91 generator.impactOccurred()
92
93 self.didImpact = true
94 }
95 }
96
97 if sender.state == .ended {
98 let velocity = sender.velocity(in: self).y
99 self.didImpact = false
100
101 if sv.contentOffset.y <= -130 {
102 self.rctRefreshCtrl?.forwarderBeginRefreshing()
103 return
104 }
105
106 // A check for a velocity under 250 prevents animations from occurring when they wouldn't in a normal
107 // scroll view
108 if abs(velocity) < 250, sv.contentOffset.y >= 0 {
109 return
110 }
111
112 self.startDecayAnimation(translation, velocity)
113 }
114 }
115
116 func startDecayAnimation(_ translation: CGFloat, _ velocity: CGFloat) {
117 guard let sv = self.rctScrollView?.scrollView else {
118 return
119 }
120
121 var velocity = velocity
122
123 self.enableCancelGestureRecognizers()
124
125 if velocity > 0 {
126 velocity = min(velocity, 5000)
127 } else {
128 velocity = max(velocity, -5000)
129 }
130
131 var animTranslation = -translation
132 self.animTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 120, repeats: true) { _ in
133 velocity *= 0.9875
134 animTranslation = (-velocity / 120) + animTranslation
135
136 let nextOffset = self.dampenOffset(animTranslation + self.initialOffset)
137
138 if nextOffset <= 0 {
139 if self.initialOffset <= 1 {
140 self.scrollToOffset(0)
141 } else {
142 sv.contentOffset.y = 0
143 }
144
145 self.stopTimer()
146 return
147 } else {
148 sv.contentOffset.y = nextOffset
149 }
150
151 if abs(velocity) < 5 {
152 self.stopTimer()
153 }
154 }
155 }
156
157 func dampenOffset(_ offset: CGFloat) -> CGFloat {
158 if offset < 0 {
159 return offset - (offset * 0.55)
160 }
161
162 return offset
163 }
164
165 func tryFindScrollView() {
166 guard let scrollViewTag = scrollViewTag else {
167 return
168 }
169
170 // Before we switch to a different scrollview, we always want to remove the cancel gesture recognizer.
171 // Otherwise we might end up with duplicates when we switch back to that scrollview.
172 self.removeCancelGestureRecognizers()
173
174 self.rctScrollView = self.appContext?
175 .findView(withTag: scrollViewTag, ofType: RCTScrollView.self)
176 self.rctRefreshCtrl = self.rctScrollView?.scrollView.refreshControl as? RCTRefreshControl
177
178 self.addCancelGestureRecognizers()
179 }
180
181 func addCancelGestureRecognizers() {
182 self.cancelGestureRecognizers?.forEach { r in
183 self.rctScrollView?.scrollView?.addGestureRecognizer(r)
184 }
185 }
186
187 func removeCancelGestureRecognizers() {
188 self.cancelGestureRecognizers?.forEach { r in
189 self.rctScrollView?.scrollView?.removeGestureRecognizer(r)
190 }
191 }
192
193 func enableCancelGestureRecognizers() {
194 self.cancelGestureRecognizers?.forEach { r in
195 r.isEnabled = true
196 }
197 }
198
199 func disableCancelGestureRecognizers() {
200 self.cancelGestureRecognizers?.forEach { r in
201 r.isEnabled = false
202 }
203 }
204
205 func scrollToOffset(_ offset: Int, animated: Bool = true) {
206 self.rctScrollView?.scroll(toOffset: CGPoint(x: 0, y: offset), animated: animated)
207 }
208
209 func stopTimer() {
210 self.disableCancelGestureRecognizers()
211 self.animTimer?.invalidate()
212 self.animTimer = nil
213 }
214}