如何正确地为 UIBezierPath 设置动画以产生水/波浪效果?

Nic*_*ick 2 uikit ios swift

我正在尝试制作一个UIBezierPath像波浪或水一样有动画效果的动画。类似于这样的事情。 https://dribbble.com/shots/3994990-Waves-Loading-Animation

我将此动画用作一种带有数据点 (0-100) 的折线图。我已正确绘制路径,但无法正确为其设置动画。

它目前看起来像这样https://imgur.com/a/QQX4DGo运动超级脊/快速

let dataPoints: [Double]

var displayLink: CADisplayLink?
var startTime: CFAbsoluteTime?

let background: UIView = {
    let view = UIView()
    return view
}()

let shapeLayer: CAShapeLayer = {
    let layer = CAShapeLayer()
    return layer
}()

init(frame: CGRect, data: [Double], precip: [String]) {
    self.dataPoints = data
    super.init(frame: frame)
    addSubview(background)
    background.anchor(top: topAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
}

override func layoutSubviews() {
    super.layoutSubviews()
    background.layer.addSublayer(shapeLayer)
    shapeLayer.strokeColor = UIColor.waterColor.cgColor
    shapeLayer.fillColor = UIColor.waterColor.cgColor
    startDisplayLink()
}

func wave(at elapsed: Double) -> UIBezierPath {
    let maxX = bounds.width
    let maxY = bounds.height

    func f(_ y: Double) -> CGFloat {
        let random = CGFloat.random(in: 1.0...5.0)
        return CGFloat(y) + sin(CGFloat(elapsed/2) * random * .pi)
    }

    func z(_ x: CGFloat) -> CGFloat {
        let random = CGFloat.random(in: 1.0...2.0)

        let position = Int.random(in: 0...1)
        if(position == 0) {
            return x + random
        } else {
            return x - random
        }
    }

    let path = UIBezierPath()
    path.move(to: CGPoint(x: 0, y: maxY))

    let steps = bounds.width/CGFloat(24)
    var start: CGFloat = steps
    for i in 0..<24 {

        let x = z(start)
        let y = maxY - f(dataPoints[i]*100)

        let point = CGPoint(x: x, y: y)
        path.addLine(to: point)

        start+=steps
    }
    path.close()
    return path
}

func startDisplayLink() {
    startTime = CFAbsoluteTimeGetCurrent()
    displayLink?.invalidate()
    displayLink = CADisplayLink(target: self, selector:#selector(handleDisplayLink(_:)))
    displayLink?.add(to: .current, forMode: .common)
    displayLink?.preferredFramesPerSecond = 11
}

func stopDisplayLink() {
    displayLink?.invalidate()
    displayLink = nil
}

@objc func handleDisplayLink(_ displayLink: CADisplayLink) {
    let elapsed = CFAbsoluteTimeGetCurrent() - startTime!
    shapeLayer.path = wave(at: elapsed).cgPath
}
Run Code Online (Sandbox Code Playgroud)

Rob*_*Rob 5

几个观察:

  1. 您正在呼叫random内部f(_:)z(_:)。这意味着每次调用其中任何一个时,每次都会得到不同的随机值。所以它会疯狂地跳来跳去。

    您希望将这些移动到常量,预先定义一次随机参数,并从那时起使用这些相同的因素。

  2. 您正在以每秒 11 帧 (fps) 的速度更新。如果您希望它平滑,请将其保留为设备默认值。

  3. 您正在渲染 24 点。除非您开始使用贝塞尔曲线(如我对您的其他问题的回答所考虑的那样),否则会产生非常粗大的输出。我会提高它(例如,在 iPhone 上,200 会产生相当平滑的波浪)。

  4. 在关闭路径之前,您缺少视图右下角的一条线。

  5. 您的波函数不太正确,返回x加上正弦函数(将产生对角波)。此外,如果你想让它感觉像波浪,我不仅会根据经过的时间改变波浪的幅度,而且还会改变整体潮汐高度。例如

    let maxAmplitude: CGFloat = 0.1
    let maxTidalVariation: CGFloat = 0.1
    let amplitudeOffset = CGFloat.random(in: -0.5 ... 0.5)
    let amplitudeChangeSpeedFactor = CGFloat.random(in: 4 ... 8)
    
    let defaultTidalHeight: CGFloat = 0.50
    let saveSpeedFactor = CGFloat.random(in: 4 ... 8)
    
    func wave(at elapsed: Double) -> UIBezierPath {
        func f(_ x: Double) -> CGFloat {
            let elapsed = CGFloat(elapsed)
            let amplitude = maxAmplitude * abs(fmod(CGFloat(elapsed/2), 3) - 1.5)
            let variation = sin((elapsed + amplitudeOffset) / amplitudeChangeSpeedFactor) * maxTidalVariation
            let value = sin((elapsed / saveSpeedFactor + CGFloat(x)) * 4 * .pi)
            return value * amplitude / 2 * bounds.height + (defaultTidalHeight + variation) * bounds.height
        }
    
        let path = UIBezierPath()
        path.move(to: CGPoint(x: bounds.minX, y: bounds.maxY))
    
        for dataPoint in dataPoints {
            let x = CGFloat(dataPoint) * bounds.width + bounds.minX
            let y = bounds.maxY - f(dataPoint)
            let point = CGPoint(x: x, y: y)
            path.addLine(to: point)
        }
        path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
        path.close()
        return path
    }
    
    Run Code Online (Sandbox Code Playgroud)
  6. 需要注意的是该文件CFAbsoluteTimeGetCurrent警告我们,

    重复调用此函数并不能保证单调递增的结果。

    我建议CACurrentMediaTime改用。

  7. 我建议dataPoints完全输掉。它没有提供任何价值。我只是继续dataPoint从视图的宽度计算。

  8. 我会坚持标准init(frame:)。这样,您既可以以编程方式添加视图,也可以直接在 Interface Builder 中添加它。

  9. 请记住使您的显示链接无效deinit

因此:

@IBDesignable
class WavyView: UIView {

    private weak var displayLink: CADisplayLink?
    private var startTime: CFTimeInterval = 0
    private let maxAmplitude: CGFloat = 0.1
    private let maxTidalVariation: CGFloat = 0.1
    private let amplitudeOffset = CGFloat.random(in: -0.5 ... 0.5)
    private let amplitudeChangeSpeedFactor = CGFloat.random(in: 4 ... 8)

    private let defaultTidalHeight: CGFloat = 0.50
    private let saveSpeedFactor = CGFloat.random(in: 4 ... 8)

    private lazy var background: UIView = {
        let background = UIView()
        background.translatesAutoresizingMaskIntoConstraints = false
        background.layer.addSublayer(shapeLayer)
        return background
    }()

    private let shapeLayer: CAShapeLayer = {
        let shapeLayer = CAShapeLayer()
        shapeLayer.strokeColor = UIColor.waterColor.cgColor
        shapeLayer.fillColor = UIColor.waterColor.cgColor
        return shapeLayer
    }()

    override init(frame: CGRect = .zero) {
        super.init(frame: frame)

        configure()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)

        configure()
    }

    override func willMove(toSuperview newSuperview: UIView?) {
        super.willMove(toSuperview: newSuperview)

        if newSuperview == nil {
            displayLink?.invalidate()
        }
   }

    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()

        shapeLayer.path = wave(at: 0)?.cgPath
    }
}

private extension WavyView {

    func configure() {
        addSubview(background)
        background.anchor(top: topAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)

        startDisplayLink()
    }

    func wave(at elapsed: Double) -> UIBezierPath? {
        guard bounds.width > 0, bounds.height > 0 else { return nil }

        func f(_ x: CGFloat) -> CGFloat {
            let elapsed = CGFloat(elapsed)
            let amplitude = maxAmplitude * abs(fmod(elapsed / 2, 3) - 1.5)
            let variation = sin((elapsed + amplitudeOffset) / amplitudeChangeSpeedFactor) * maxTidalVariation
            let value = sin((elapsed / saveSpeedFactor + x) * 4 * .pi)
            return value * amplitude / 2 * bounds.height + (defaultTidalHeight + variation) * bounds.height
        }

        let path = UIBezierPath()
        path.move(to: CGPoint(x: bounds.minX, y: bounds.maxY))

        let count = Int(bounds.width / 10)

        for step in 0 ... count {
            let dataPoint = CGFloat(step) / CGFloat(count)
            let x = dataPoint * bounds.width + bounds.minX
            let y = bounds.maxY - f(dataPoint)
            let point = CGPoint(x: x, y: y)
            path.addLine(to: point)
        }
        path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
        path.close()
        return path
    }

    func startDisplayLink() {
        startTime = CACurrentMediaTime()
        displayLink?.invalidate()
        let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
        displayLink.add(to: .main, forMode: .common)
        self.displayLink = displayLink
    }

    func stopDisplayLink() {
        displayLink?.invalidate()
    }

    @objc func handleDisplayLink(_ displayLink: CADisplayLink) {
        let elapsed = CACurrentMediaTime() - startTime
        shapeLayer.path = wave(at: elapsed)?.cgPath
    }
}
Run Code Online (Sandbox Code Playgroud)

这产生:

在此处输入图片说明

  • @Nick - 我在该屏幕快照中没有看到任何“波浪”,但是是的,整个波浪可以有不同的高度。上面的动画曲线是一条简单的正弦曲线。但是,如果您将“f”设为两条不同正弦曲线的总和(例如,也许第二条正弦曲线的振幅较小,但周期较长)。根据周期、幅度等,您可能会得到各种变化。例如,这是一个微妙的:https://gist.github.com/robertmryan/427e7d4d74f4c4e878c8755e68fd9d13 (2认同)