使用来自 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 的数据

使用来自两个数据库的自动增量 ID 连接来自两个表的数据

显示来自 localStorage 缓存的数据,然后使用来自服务的 AJAX 结果进行更新

如何使用 RestTemplate 在 Spring MVC 应用程序中访问来自(来自 Spring RESTful 服务)的巨大 JSON

Liferay,来自搜索的 Freemarker 错误模板,但不是来自其他链接

使用来自 IBM MQ 的所有消息