Bluesky app fork with some witchin' additions 馃挮
at main 214 lines 6.4 kB view raw
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}