-
-
Save levantAJ/7860d3053324e4fc20e12d5dd3c51fb1 to your computer and use it in GitHub Desktop.
// | |
// ScrollingDecelerator.swift | |
// ShopBack | |
// | |
// Created by Tai Le on 6/5/20. | |
// Copyright © 2020 levantAJ. All rights reserved. | |
// | |
final class ScrollingDecelerator { | |
weak var scrollView: UIScrollView? | |
var scrollingAnimation: TimerAnimationProtocol? | |
let threshold: CGFloat | |
init(scrollView: UIScrollView) { | |
self.scrollView = scrollView | |
threshold = 0.1 | |
} | |
} | |
// MARK: - ScrollingDeceleratorProtocol | |
extension ScrollingDecelerator: ScrollingDeceleratorProtocol { | |
func decelerate(by deceleration: ScrollingDeceleration) { | |
guard let scrollView = scrollView else { return } | |
let velocity = CGPoint(x: deceleration.velocity.x, y: deceleration.velocity.y * 1000 * threshold) | |
scrollingAnimation = beginScrollAnimation(initialContentOffset: scrollView.contentOffset, initialVelocity: velocity, decelerationRate: deceleration.decelerationRate.rawValue) { [weak scrollView] point in | |
guard let scrollView = scrollView else { return } | |
if deceleration.velocity.y < 0 { | |
scrollView.contentOffset.y = max(point.y, 0) | |
} else { | |
scrollView.contentOffset.y = max(0, min(point.y, scrollView.contentSize.height - scrollView.frame.height)) | |
} | |
} | |
} | |
func invalidateIfNeeded() { | |
guard scrollView?.isUserInteracted == true else { return } | |
scrollingAnimation?.invalidate() | |
scrollingAnimation = nil | |
} | |
} | |
// MARK: - Privates | |
extension ScrollingDecelerator { | |
private func beginScrollAnimation(initialContentOffset: CGPoint, initialVelocity: CGPoint, | |
decelerationRate: CGFloat, | |
animations: @escaping (CGPoint) -> Void) -> TimerAnimationProtocol { | |
let timingParameters = ScrollTimingParameters(initialContentOffset: initialContentOffset, | |
initialVelocity: initialVelocity, | |
decelerationRate: decelerationRate, | |
threshold: threshold) | |
return TimerAnimation(duration: timingParameters.duration, animations: { progress in | |
let point = timingParameters.point(at: progress * timingParameters.duration) | |
animations(point) | |
}) | |
} | |
} | |
// MARK: - ScrollTimingParameters | |
extension ScrollingDecelerator { | |
struct ScrollTimingParameters { | |
let initialContentOffset: CGPoint | |
let initialVelocity: CGPoint | |
let decelerationRate: CGFloat | |
let threshold: CGFloat | |
} | |
} | |
extension ScrollingDecelerator.ScrollTimingParameters { | |
var duration: TimeInterval { | |
guard decelerationRate < 1 | |
&& decelerationRate > 0 | |
&& initialVelocity.length != 0 else { return 0 } | |
let dCoeff = 1000 * log(decelerationRate) | |
return TimeInterval(log(-dCoeff * threshold / initialVelocity.length) / dCoeff) | |
} | |
func point(at time: TimeInterval) -> CGPoint { | |
guard decelerationRate < 1 | |
&& decelerationRate > 0 | |
&& initialVelocity != .zero else { return .zero } | |
let dCoeff = 1000 * log(decelerationRate) | |
return initialContentOffset + (pow(decelerationRate, CGFloat(1000 * time)) - 1) / dCoeff * initialVelocity | |
} | |
} | |
// MARK: - TimerAnimation | |
extension ScrollingDecelerator { | |
final class TimerAnimation { | |
typealias Animations = (_ progress: Double) -> Void | |
typealias Completion = (_ isFinished: Bool) -> Void | |
weak var displayLink: CADisplayLink? | |
private(set) var isRunning: Bool | |
private let duration: TimeInterval | |
private let animations: Animations | |
private let completion: Completion? | |
private let firstFrameTimestamp: CFTimeInterval | |
init(duration: TimeInterval, animations: @escaping Animations, completion: Completion? = nil) { | |
self.duration = duration | |
self.animations = animations | |
self.completion = completion | |
firstFrameTimestamp = CACurrentMediaTime() | |
isRunning = true | |
let displayLink = CADisplayLink(target: self, selector: #selector(step)) | |
displayLink.add(to: .main, forMode: .common) | |
self.displayLink = displayLink | |
} | |
} | |
} | |
// MARK: - TimerAnimationProtocol | |
extension ScrollingDecelerator.TimerAnimation: TimerAnimationProtocol { | |
func invalidate() { | |
guard isRunning else { return } | |
isRunning = false | |
stopDisplayLink() | |
completion?(false) | |
} | |
} | |
// MARK: - Privates | |
extension ScrollingDecelerator.TimerAnimation { | |
@objc private func step(displayLink: CADisplayLink) { | |
guard isRunning else { return } | |
let elapsed = CACurrentMediaTime() - firstFrameTimestamp | |
if elapsed >= duration | |
|| duration == 0 { | |
animations(1) | |
isRunning = false | |
stopDisplayLink() | |
completion?(true) | |
} else { | |
animations(elapsed / duration) | |
} | |
} | |
private func stopDisplayLink() { | |
displayLink?.isPaused = true | |
displayLink?.invalidate() | |
displayLink = nil | |
} | |
} | |
// MARK: - CGPoint | |
private extension CGPoint { | |
var length: CGFloat { | |
return sqrt(x * x + y * y) | |
} | |
static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint { | |
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) | |
} | |
static func * (lhs: CGFloat, rhs: CGPoint) -> CGPoint { | |
return CGPoint(x: lhs * rhs.x, y: lhs * rhs.y) | |
} | |
} | |
final class ScrollingDeceleration { | |
let velocity: CGPoint | |
let decelerationRate: UIScrollView.DecelerationRate | |
init(velocity: CGPoint, decelerationRate: UIScrollView.DecelerationRate) { | |
self.velocity = velocity | |
self.decelerationRate = decelerationRate | |
} | |
} | |
// MARK: - Equatable | |
extension ScrollingDeceleration: Equatable { | |
static func == (lhs: ScrollingDeceleration, rhs: ScrollingDeceleration) -> Bool { | |
return lhs.velocity == rhs.velocity | |
&& lhs.decelerationRate == rhs.decelerationRate | |
} | |
} | |
// MARK: - protocol ScrollingDeceleratorProtocol { | |
func decelerate(by deceleration: ScrollingDeceleration) | |
func invalidateIfNeeded() | |
} | |
// MARK: - TimerAnimationProtocol | |
protocol TimerAnimationProtocol { | |
func invalidate() | |
} | |
// MARK: - UIScrollView | |
extension UIScrollView { | |
// Indicates that the scrolling is caused by user. | |
var isUserInteracted: Bool { | |
return isTracking || isDragging || isDecelerating | |
} | |
} |
Compile failed, Use of undeclared type TimerAnimationProtocol、CADisplayLinkProtocol、ScrollingDeceleratorProtocol、DateRepositoryProtocol
Thanks @jabbarwockeez, updated!
DateRepositoryProtocol is missing. Can you please check?
Thanks for your work, but compile failed. I think you missed DateRepositoryProtocol, can you add it, please?
hi @VikasPrajapatiSA @Frace17 the DateRepositoryProtocol was removed and updated as above!
I still cant achieve the correct behavior of scrollviews, can you provide full example, please?
dateRepository is not declared anywhere. Could you please check?
Updated the gist @eshwavin
UIScrollView has no member isUserInteracted. Can we replace it with scrollView.isTracking ?
Just updated the gist @exera
var isUserInteracted: Bool {
return isTracking || isDragging || isDecelerating
}
Thanks for letting me know !
Would you please provide a demo ?
Hello
Please replace line 188 with this :
// MARK: - ScrollingDeceleratorProtocol
protocol ScrollingDeceleratorProtocol {
How to use: