267cc37e60
This reverts commit 2e2fcaeeed.
231 lines
6.7 KiB
Swift
231 lines
6.7 KiB
Swift
import ExpoModulesCore
|
||
|
||
// This view will be used as a native component. Make sure to inherit from `ExpoView`
|
||
// to apply the proper styling (e.g. border radius and shadows).
|
||
class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
|
||
var scrollViewTag: Int? {
|
||
didSet {
|
||
self.tryFindScrollView()
|
||
}
|
||
}
|
||
|
||
private var scrollView: UIScrollView?
|
||
private var rctRefreshCtrl: RCTRefreshControl?
|
||
private var cancelGestureRecognizers: [UIGestureRecognizer]?
|
||
private var animTimer: Timer?
|
||
private var initialOffset: CGFloat = 0.0
|
||
private var didImpact: Bool = false
|
||
|
||
required init(appContext: AppContext? = nil) {
|
||
super.init(appContext: appContext)
|
||
|
||
let pg = UIPanGestureRecognizer(target: self, action: #selector(callOnPan(_:)))
|
||
pg.delegate = self
|
||
self.addGestureRecognizer(pg)
|
||
|
||
let tg = UITapGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
|
||
tg.isEnabled = false
|
||
tg.delegate = self
|
||
|
||
let lpg = UILongPressGestureRecognizer(target: self, action: #selector(callOnPress(_:)))
|
||
lpg.minimumPressDuration = 0.01
|
||
lpg.isEnabled = false
|
||
lpg.delegate = self
|
||
|
||
self.cancelGestureRecognizers = [lpg, tg]
|
||
}
|
||
|
||
// We don't want to recognize the scroll pan gesture and the swipe back gesture together
|
||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||
if gestureRecognizer is UIPanGestureRecognizer, otherGestureRecognizer is UIPanGestureRecognizer {
|
||
return false
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
// We only want the "scroll" gesture to happen whenever the pan is vertical, otherwise it will
|
||
// interfere with the native swipe back gesture.
|
||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||
guard let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else {
|
||
return true
|
||
}
|
||
|
||
let velocity = gestureRecognizer.velocity(in: self)
|
||
return abs(velocity.y) > abs(velocity.x)
|
||
}
|
||
|
||
// This will be used to cancel the scroll animation whenever we tap inside of the header. We don't need another
|
||
// recognizer for this one.
|
||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||
self.stopTimer()
|
||
}
|
||
|
||
// This will be used to cancel the animation whenever we press inside of the scroll view. We don't want to change
|
||
// the scroll view gesture's delegate, so we add an additional recognizer to detect this.
|
||
@IBAction func callOnPress(_ sender: UITapGestureRecognizer) {
|
||
self.stopTimer()
|
||
}
|
||
|
||
@IBAction func callOnPan(_ sender: UIPanGestureRecognizer) {
|
||
guard let sv = self.scrollView else {
|
||
return
|
||
}
|
||
|
||
let translation = sender.translation(in: self).y
|
||
|
||
if sender.state == .began {
|
||
if sv.contentOffset.y < 0 {
|
||
sv.contentOffset.y = 0
|
||
}
|
||
|
||
self.initialOffset = sv.contentOffset.y
|
||
}
|
||
|
||
if sender.state == .changed {
|
||
sv.contentOffset.y = self.dampenOffset(-translation + self.initialOffset)
|
||
|
||
if sv.contentOffset.y <= -130, !didImpact {
|
||
let generator = UIImpactFeedbackGenerator(style: .light)
|
||
generator.impactOccurred()
|
||
|
||
self.didImpact = true
|
||
}
|
||
}
|
||
|
||
if sender.state == .ended {
|
||
let velocity = sender.velocity(in: self).y
|
||
self.didImpact = false
|
||
|
||
if sv.contentOffset.y <= -130 {
|
||
self.rctRefreshCtrl?.forwarderBeginRefreshing()
|
||
return
|
||
}
|
||
|
||
// A check for a velocity under 250 prevents animations from occurring when they wouldn't in a normal
|
||
// scroll view
|
||
if abs(velocity) < 250, sv.contentOffset.y >= 0 {
|
||
return
|
||
}
|
||
|
||
self.startDecayAnimation(translation, velocity)
|
||
}
|
||
}
|
||
|
||
func startDecayAnimation(_ translation: CGFloat, _ velocity: CGFloat) {
|
||
guard let sv = self.scrollView else {
|
||
return
|
||
}
|
||
|
||
var velocity = velocity
|
||
|
||
self.enableCancelGestureRecognizers()
|
||
|
||
if velocity > 0 {
|
||
velocity = min(velocity, 5000)
|
||
} else {
|
||
velocity = max(velocity, -5000)
|
||
}
|
||
|
||
var animTranslation = -translation
|
||
self.animTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 120, repeats: true) { _ in
|
||
velocity *= 0.9875
|
||
animTranslation = (-velocity / 120) + animTranslation
|
||
|
||
let nextOffset = self.dampenOffset(animTranslation + self.initialOffset)
|
||
|
||
if nextOffset <= 0 {
|
||
if self.initialOffset <= 1 {
|
||
self.scrollToOffset(0)
|
||
} else {
|
||
sv.contentOffset.y = 0
|
||
}
|
||
|
||
self.stopTimer()
|
||
return
|
||
} else {
|
||
sv.contentOffset.y = nextOffset
|
||
}
|
||
|
||
if abs(velocity) < 5 {
|
||
self.stopTimer()
|
||
}
|
||
}
|
||
}
|
||
|
||
func dampenOffset(_ offset: CGFloat) -> CGFloat {
|
||
if offset < 0 {
|
||
return offset - (offset * 0.55)
|
||
}
|
||
|
||
return offset
|
||
}
|
||
|
||
private func findScrollView(in view: UIView, foundCount: Int) -> UIScrollView? {
|
||
var foundCount = foundCount
|
||
if let sv = view as? UIScrollView { return sv }
|
||
for child in view.subviews {
|
||
if let found = findScrollView(in: child, foundCount: foundCount) {
|
||
if foundCount == 1 {
|
||
print("found sv: \(found)")
|
||
// return found
|
||
} else {
|
||
print("found sv: \(found)")
|
||
foundCount += 1
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func tryFindScrollView() {
|
||
// Before we switch to a different scrollview, we always want to remove the cancel gesture recognizer.
|
||
// Otherwise we might end up with duplicates when we switch back to that scrollview.
|
||
self.removeCancelGestureRecognizers()
|
||
|
||
guard let sv = self.findScrollView(in: self.superview!.superview!.superview!, foundCount: 0) else {
|
||
print("⚠️ ExpoScrollForwarder: couldn’t find UIScrollView under tag \(tag)")
|
||
return
|
||
}
|
||
|
||
self.scrollView = sv
|
||
self.rctRefreshCtrl = sv.refreshControl as? RCTRefreshControl
|
||
|
||
self.addCancelGestureRecognizers()
|
||
}
|
||
|
||
func addCancelGestureRecognizers() {
|
||
self.cancelGestureRecognizers?.forEach { r in
|
||
self.scrollView?.addGestureRecognizer(r)
|
||
}
|
||
}
|
||
|
||
func removeCancelGestureRecognizers() {
|
||
self.cancelGestureRecognizers?.forEach { r in
|
||
self.scrollView?.removeGestureRecognizer(r)
|
||
}
|
||
}
|
||
|
||
func enableCancelGestureRecognizers() {
|
||
self.cancelGestureRecognizers?.forEach { r in
|
||
r.isEnabled = true
|
||
}
|
||
}
|
||
|
||
func disableCancelGestureRecognizers() {
|
||
self.cancelGestureRecognizers?.forEach { r in
|
||
r.isEnabled = false
|
||
}
|
||
}
|
||
|
||
func scrollToOffset(_ offset: Int, animated: Bool = true) {
|
||
self.scrollView?.scrollRectToVisible(CGRect(x: 0, y: offset, width: 0, height: 0), animated: animated)
|
||
}
|
||
|
||
func stopTimer() {
|
||
self.disableCancelGestureRecognizers()
|
||
self.animTimer?.invalidate()
|
||
self.animTimer = nil
|
||
}
|
||
}
|