我正在研究一种滚动算法,该算法从 UIPanGestureRecognizer 获取输入,以在自定义 UIView 中移动绘制的对象。我希望滚动感觉像 UIScrollView,而不是感觉有点笨重。当在平移手势后抬起手指并且粒子减速直到停止时,我在实现这种情况时遇到问题。
算法引起的减速度似乎有时会有所不同。有时看起来它实际上在减速之前会加速一点。
我想知道你们是否对如何解决这个问题有一些建议?
该算法的工作原理如下:
当识别器状态为“开始”时,粒子的当前位置存储在变量startPosition中。
当识别器状态“改变”时,粒子的当前位置是相对于运动来计算的。当前速度被存储, View 被更新以在新位置显示粒子。
当识别器状态“结束”时,我们使用 GCD API 进入全局线程来计算剩余移动(这是为了防止 UI 卡住)。循环启动并设置为以 60 fps 运行。每次迭代都会更新粒子位置并降低速度。我们进入主线程以更新 View 以将粒子显示在新位置。
func scroll(recognizer: UIPanGestureRecognizer, view: UIView) {
switch recognizer.state {
case .began:
startPosition = currentPosition
case .changed:
currentPosition = startPosition + recognizer.translation(in: view)
velocity = recognizer.velocity(in: view)
view.setNeedsDisplay()
case .ended:
DispatchQueue.global().async {
let fps: Double = 60
let delayTime: Double = 1 / fps
var frameStart = CACurrentMediaTime()
let friction: CGFloat = 0.9
let tinyValue: CGFloat = 0.001
while (self.velocity > tinyValue) {
self.currentPosition += (self.velocity / CGFloat(fps))
DispatchQueue.main.sync {
view.setNeedsDisplay()
}
self.velocity *= friction
let frameTime = CACurrentMediaTime() - frameStart
if (frameTime < delayTime) {
// calculate time to sleep in μs
usleep(UInt32((delayTime - frameTime) * 1E6))
}
frameStart = CACurrentMediaTime()
}
}
default:
return
}
}
问题可能是循环没有以非常稳定的帧速率运行。但帧速率对我来说看起来足够稳定(我可能是错的)。以下是计算的帧速率的示例:
"
Frame rate: 59.447833329705766
Frame rate: 57.68833849362473
Frame rate: 57.43794057083063
Frame rate: 53.11410092668673
Frame rate: 51.76492245230155
Frame rate: 52.71845062546561
Frame rate: 50.211233616282904
Frame rate: 59.86028817338459
Frame rate: 55.7360938798143
Frame rate: 47.55385819651489
Frame rate: 50.13437540167264
Frame rate: 48.93274027995551
Frame rate: 50.76905714109756
Frame rate: 57.06095686426517
Frame rate: 49.852101165412876
Frame rate: 51.49043459888154
Frame rate: 55.96442240956844
Frame rate: 53.66651780498373
Frame rate: 55.336349953967726
Frame rate: 51.4698476880566
"
通过在循环末尾、更新 frameStart 变量之前添加以下行来计算:
print("帧速率:\(1/(CACurrentMediaTime() - frameStart))")
关于如何使帧速率更加稳定有什么建议吗?
竞争条件可能是问题所在,但currentPosition(用于定位粒子)变量受信号量保护。而且我无法(据我所知)代码中的任何其他关键区域。
var currentPosition: CGPoint {
get {
semaphore.wait()
let pos = _currentPosition
semaphore.signal()
return pos
}
set {
semaphore.wait()
_currentPosition = newValue
semaphore.signal()
}
}
我很高兴听到任何建议。谢谢!
最佳答案
根据 @CraigSiemens 的建议,我能够使用 CADisplayLink 使动画非常流畅。非常感谢@CraigSiemens!仍然不知道我的 CADisplayLink 实现是否是最终的解决方案,但至少是一个很大的改进。解决方案如下:
ViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
displayLink = CADisplayLink(target: self, selector: #selector(step))
displayLink.add(to: .current, forMode: .default)
}
deinit {
displayLink.invalidate()
}
@objc func step(displayLink: CADisplayLink) {
InputHandler.shared.decelerate(displayLink: displayLink)
drawView.setNeedsDisplay()
}
InputHandler.swift
func scroll(recognizer: UIPanGestureRecognizer, view: UIView, displayLink: CADisplayLink) {
switch recognizer.state {
case .began:
decelerate = false
displayLink.isPaused = false
startPosition = currentPosition
case .changed:
currentPosition = startPosition + recognizer.translation(in: view)
velocity = recognizer.velocity(in: view)
case .ended:
decelerate = true
default:
return
}
}
func decelerate(displayLink: CADisplayLink) {
if decelerate {
let friction: CGFloat = 0.9
let delayTime = displayLink.targetTimestamp - displayLink.timestamp
let fps = 1 / delayTime
self.currentPosition += (self.velocity / CGFloat(fps))
self.velocity *= friction
if (self.velocity < 0.01) {
decelerate = false
displayLink.isPaused = true
}
}
}
关于swift - 使用 UIPanGestureRecognizer 的输入实现自然滚动算法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56908958/