Реализация естественного алгоритма прокрутки с использованием ввода из UIPanGestureRecognizer - PullRequest
0 голосов
/ 05 июля 2019

Я работаю над алгоритмом прокрутки, который принимает входные данные из UIPanGestureRecognizer для перемещения нарисованного объекта в пользовательском UIView.Я хочу, чтобы прокрутка была похожа на UIScrollView, вместо этого она выглядит неуклюжей.У меня проблема с реализацией случая, когда палец поднимается после жеста панорамирования, и частица замедляется, пока не остановится.

Замедление, вызванное алгоритмом, кажется, время от времени различается.Иногда кажется, что он на самом деле немного ускоряется, прежде чем замедляется.

Мне интересно, есть ли у вас какие-нибудь предложения, как это исправить?

Алгоритм работает следующим образом:

  • Когда состояние распознавателя равно«Начала» текущая позиция частицы сохраняется в переменной startPosition .

  • Пока состояние распознавателя «изменено», текущее положение частиц рассчитывается относительно движения.Текущая скорость сохраняется, и представление обновляется, чтобы отобразить частицу в новом месте.

  • Когда состояние распознавателя «закончено», мы входим в глобальный поток, используя GCD API для вычисленияоставшееся движение (чтобы предотвратить зависание интерфейса) .Цикл запускается и устанавливается на 60 кадров в секунду.Положение частиц обновляется, и скорость уменьшается на каждой итерации.Мы входим в основной поток, чтобы обновить представление для отображения частицы в ее новой позиции.

    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 (которыйиспользуется для позиционирования частицы) переменная защищена семафором.И я не могу (из-за моего недостатка знаний) se других критических областей в коде.

var currentPosition: CGPoint {
    get {
        semaphore.wait()
        let pos = _currentPosition
        semaphore.signal()
        return pos
    }
    set {
        semaphore.wait()
        _currentPosition = newValue
        semaphore.signal()
    }
}

Я рад услышать любые предложения.Спасибо!

1 Ответ

0 голосов
/ 06 июля 2019

С предложением @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
        }
    }
}
...