-
Notifications
You must be signed in to change notification settings - Fork 40
/
Copy pathUIScrollView+velocity.swift
68 lines (58 loc) · 2.74 KB
/
UIScrollView+velocity.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
//
// UIScrollView+velocity.swift
// UIKit
//
// Created by flowing erik on 25.09.17.
// Copyright © 2017 flowkey. All rights reserved.
//
private extension CGPoint {
var magnitude: CGFloat {
return (x * x + y * y).squareRoot()
}
}
extension UIScrollView {
func startDeceleratingIfNecessary() {
// Only animate if instantaneous velocity is large enough
// Otherwise we could animate after scrolling quickly, pausing for a few seconds, then letting go
let velocityIsLargeEnoughToDecelerate = (self.panGestureRecognizer.velocity(in: self).magnitude > 10)
let dampingFactor = 0.5 // hand-tuned
let nonBoundsCheckedScrollAnimationDistance = self.weightedAverageVelocity * CGFloat(dampingFactor) // hand-tuned
let targetOffset = getBoundsCheckedContentOffset(contentOffset - nonBoundsCheckedScrollAnimationDistance)
let distanceToBoundsCheckedTarget = contentOffset - targetOffset
let willDecelerate = (velocityIsLargeEnoughToDecelerate && distanceToBoundsCheckedTarget.magnitude > 0.0)
delegate?.scrollViewDidEndDragging(self, willDecelerate: willDecelerate)
guard willDecelerate else { hideScrollIndicators(); return }
// https://ariya.io/2011/10/flick-list-with-its-momentum-scrolling-and-deceleration
// TODO: This value should be calculated from `self.decelerationRate` instead
// But actually we want to redo this function to avoid `UIView.animate` entirely,
// in which case we wouldn't need an animationTime at all.
let animationTimeConstant = 0.325 * dampingFactor
// This calculation is a weird approximation but it's close enough for now...
let animationTime = log(Double(distanceToBoundsCheckedTarget.magnitude)) * animationTimeConstant
UIView.animate(
withDuration: animationTime,
options: [.beginFromCurrentState, .customEaseOut, .allowUserInteraction],
animations: {
self.isDecelerating = true
self.contentOffset = targetOffset
},
completion: { _ in
self.isDecelerating = false
}
)
}
func cancelDeceleratingIfNeccessary() {
if !isDecelerating { return }
// Get the presentation value from the current animation
setContentOffset(visibleContentOffset, animated: false)
cancelDecelerationAnimations()
isDecelerating = false
}
func cancelDecelerationAnimations() {
if !layer.animations.isEmpty {
layer.removeAnimation(forKey: "bounds")
horizontalScrollIndicator.layer.removeAnimation(forKey: "position")
verticalScrollIndicator.layer.removeAnimation(forKey: "position")
}
}
}