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

2024-05-08

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

我将此动画用作一种带有数据点 (0-100) 的折线图。我已经正确绘制了路径,但在正确设置动画时遇到问题。

目前看起来像这样https://i.stack.imgur.com/jXLEK.jpg https://i.stack.imgur.com/jXLEK.jpg运动超级有脊/快速

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
}

一些观察结果:

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

    您希望将它们移至常量,预先定义随机参数,然后从那时起使用这些相同的因子。

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

  3. 您正在渲染 24 点。除非你开始使用贝塞尔曲线(正如我在回答你的另一个问题 https://stackoverflow.com/a/60744144/1271826),这将产生非常大的输出。我会把它调高(例如,在 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
    }
    
  6. 注意文档 https://developer.apple.com/documentation/corefoundation/1543542-cfabsolutetimegetcurrent for CFAbsoluteTimeGetCurrent警告我们

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

    我建议使用CACurrentMediaTime https://developer.apple.com/documentation/quartzcore/1395996-cacurrentmediatime?language=occ反而。

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

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

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

Thus:

@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
    }
}

得出:

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何正确设置 UIBezierPath 的动画以产生水/波浪效果? 的相关文章

随机推荐

  • 为什么 Jqgrid 冻结列似乎不能与过滤器行和过滤器标题一起使用?

    我无法让冻结列与 jqgrid 4 3 0 一起使用 我唯一能想到的是我有一些不是开箱即用的具体东西 我在顶部使用过滤行 我使用 cloneToTop true 显示网格顶部的所有按钮 我有以下代码 用于在 jqggrid 顶部显示过滤器状
  • ReportViewer“缺少 URL 参数:名称”

    在一个网络应用程序中 我正在处理 ReportViewer 时不断出现错误 缺少 URL 参数 名称 我找到了原因 但没有找到解决方案 导致报告查看器出现异常的 url Reserved ReportViewerWebControl axd
  • 删除 matplotlib 中的行

    我需要删除子图上的所有线条 然后重新绘制它们 我正在创建一个重绘函数 以便在添加 删除某些线条时使用 我该怎么做 如果您有Axes存储的对象 您可以通过lines member ax fig add subplot 111 ax plot
  • 运行 R.exe 会创建临时文件吗?

    我在想 是否启动 R exewindows创建临时文件并 是否解释类似x lt 5写入那些临时文件 如果创建了临时文件 它们存储在哪里 如果我启动多个 R exe 实例会发生什么情况 他们会共享并覆盖彼此的临时文件吗 R 的每个实例都有自己
  • 计算网站上多个文件的下载次数的最佳方法[关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 问题是 计算网站上多个文件的下载次数的最佳方法 我正在尝试做的事情 跟踪并统计多个文件的下载数量 对于具有不同扩展名的文件 foo z
  • 如何在 Laravel 中返​​回唯一值

    这里我有这个示例数据 它根据类别产品返回 我需要限制重复值 Raw JSON brand id fe877b45 8620 453a 8805 63f0cbd80752 name No Brand slug no brand descrip
  • 字符串中unicode字符的正则表达式

    我正在使用 C 进行一些 OCR 工作 并提取了我需要使用的文本 现在我需要使用正则表达式解析一行 string checkNum string routingNum string accountNum Regex regEx new Re
  • Collection.Contains() 使用什么来检查现有对象?

    我有一个强类型的自定义对象列表 MyObject 它有一个属性Id 以及一些其他属性 假设Id of a MyObject将其定义为唯一 我想检查我的收藏是否还没有MyObject对象有一个Id在我添加新的之前 共 1 个MyObject到
  • Tensorflow:获取为零的数组行索引

    对于张量 1 2 3 1 0 0 0 0 1 3 5 7 0 0 0 0 3 5 7 8 如何获取 0 行的索引 IE 列表 1 3 在 Tensorflow 中 据我所知 您无法像使用 NumPy 等更高级的库那样在一个命令中真正做到这一
  • 默认可变参数的惯用方式

    在 python 中 如果直接将可变类型设置为默认参数 则会出现众所周知的边缘情况 def foo x return x y foo y append 1 print foo 通常的解决方法是将参数默认为None然后将其放入体内 然而 有
  • Facebook 注册后重定向至页面

    我正在尝试在成功 Facebook 注册 未登录 后重定向用户 我想重定向到 getstarted welcome用户注册后首次 我的omniauth回调是 def facebook You need to implement the me
  • 为什么 $_SERVER["PHP_AUTH_USER"] 和 $_SERVER["PHP_AUTH_PW"] 没有设置?

    在开始之前 我想指出我浏览过 Stack Overflow 并发现了其他类似的问题 PHP AUTH USER 未设置 https stackoverflow com questions 3663520 php auth user not
  • 无法运行bjam编译boost python教程

    我正在尝试跟随本教程 http www boost org doc libs 1 55 0 libs python doc tutorial doc html python hello html关于为 Windows 的 python 包装
  • 数据库函数 VS Case 语句

    昨天我们遇到了一个场景 必须获取 a 的类型db field在此基础上我们必须编写该字段的描述 喜欢 Select Case DB Type When I Then Intermediate When P Then Pending Else
  • 当使用 loadView 创建视图时,视图的框架大小在旋转时不会改变

    我有没有 xib 的 UIViewController 并且我正在使用 loadView 来构建创建并添加两个滚动视图的 UI 问题是 当旋转发生时 主视图框架的大小不会改变 我的意思是 我在 loadView 中设置主视图的初始帧大小 纵
  • 为什么 LayoutInflater 忽略我指定的layout_width 和layout_height 布局参数?

    我在让 LayoutInflater 按预期工作时遇到了严重的困难 其他人也是如此 如何使用layoutinflator在运行时添加视图 https stackoverflow com questions 4735847 help for
  • 如何获得字符串的所有字谜

    我试图找到一个字符串的所有可能的字谜并仅使用递归将它们存储在数组中 我被困住了 这就是我所拥有的一切 int main const int MAX 10 string a ABCD string arr 10 permute arr a 0
  • jstack 是否停止在较新的 JDK8 版本上工作?

    我惊讶地发现 不知何故 最近 jstack 停止了在较新的 JDK 8 上的工作 我不确定这发生在哪个版本 但我确实得到 36649 Unable to open socket file target process not respond
  • 反向代理受 NTLM 保护的网站

    如何将请求代理到受 NTLM 保护的网站 例如团队基金会 and 共享点 我不断得到401 身份验证错误 根据这篇 Microsoft TechNet 文章 https www microsoft com technet prodtechn
  • 如何正确设置 UIBezierPath 的动画以产生水/波浪效果?

    我正在尝试做一个UIBezierPath像波浪或水一样动画 类似于这样的事情 https dribbble com shots 3994990 Waves Loading Animation https dribbble com shots