使用来自 UIPanGestureRecognizer 的输入实现自然滚动算法
Posted
技术标签:
【中文标题】使用来自 UIPanGestureRecognizer 的输入实现自然滚动算法【英文标题】:Implementing a natural scrolling algorithm using input from UIPanGestureRecognizer 【发布时间】:2019-07-05 20:32:58 【问题描述】:我正在研究一种滚动算法,该算法从 UIPanGestureRecognizer 获取输入以在自定义 UIView 中移动绘制的对象。我希望滚动感觉像 UIScrollView,而不是感觉有点笨拙。在平移手势后抬起手指并且粒子减速直到停止时,我在实现这种情况时遇到了问题。
算法造成的减速似乎不时变化。有时它似乎在减速之前实际上加速了一点。
我想知道你们是否对如何解决这个问题有一些建议?
算法的工作原理如下:
当识别器状态为“开始”时,粒子的当前位置存储在变量 startPosition 中。
当识别器状态“改变”时,粒子当前位置是相对于运动计算的。存储当前速度并更新视图以在新位置显示粒子。
当识别器状态为“结束”时,我们进入一个使用 GCD API 计算剩余运动的全局线程(这是为了防止 UI 冻结)。开始循环并设置为以 60 fps 运行。每次迭代都会更新粒子位置并降低速度。我们进入主线程以更新视图以在新位置显示粒子。
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("Frame rate: \(1 / (CACurrentMediaTime() - frameStart))")
有关如何使帧速率更加稳定的任何建议?
竞争条件可能是问题所在,但 currentPosition (用于定位粒子) 变量受信号量保护。而且我无法(据我所知)查看代码中的任何其他关键区域。
var currentPosition: CGPoint
get
semaphore.wait()
let pos = _currentPosition
semaphore.signal()
return pos
set
semaphore.wait()
_currentPosition = newValue
semaphore.signal()
我很高兴听到任何建议。谢谢!
【问题讨论】:
如果您使用CADisplayLink
而不是您自己的循环,您可能会获得更好的准确性。此外,在帧速率方面,60fps 和 59fps 之间存在很大差异,因为这将使动画的一个步骤可见的时间是其他步骤的两倍,这会使其感觉不那么流畅。
【参考方案1】:
在@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
【讨论】:
以上是关于使用来自 UIPanGestureRecognizer 的输入实现自然滚动算法的主要内容,如果未能解决你的问题,请参考以下文章
需要帮助使用来自 DataTable 的数据处理来自 MySQL 的数据
显示来自 localStorage 缓存的数据,然后使用来自服务的 AJAX 结果进行更新
如何使用 RestTemplate 在 Spring MVC 应用程序中访问来自(来自 Spring RESTful 服务)的巨大 JSON