swift - 使用 UIPanGestureRecognizer 的输入实现自然滚动算法

标签 swift uikit uigesturerecognizer grand-central-dispatch uipangesturerecognizer

我正在研究一种滚动算法,该算法从 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/

相关文章:

uikit - 如何检测祖先是否专注于 tvOS?

uicollectionview - iOS7应用管理器中的UICollectionView "swipe-away"?

ios - 自定义 UITableViewCell 上的 UIGestureRecognizer。轻敲多个细胞

swift - 禁止直接赋值的可变属性 - Swift

ios - 如何从另一个 UIViewController 启动 SLComposeServiceViewController

ios - 带有渐变的 CAShapeLayer

ios - UIGestureRecognizer 确定点击

ios - 如果我的设备中未安装 Instagram,则 Instagram 共享打开另一个应用程序

ios - 如何更新 UILabel?

ios - 具有自动调整单元格大小的自定义 UICollectionViewLayout 会因较大的估​​计项目高度而中断