我正在尝试制作一个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)
几个观察:
您正在呼叫random内部f(_:)和z(_:)。这意味着每次调用其中任何一个时,每次都会得到不同的随机值。所以它会疯狂地跳来跳去。
您希望将这些移动到常量,预先定义一次随机参数,并从那时起使用这些相同的因素。
您正在以每秒 11 帧 (fps) 的速度更新。如果您希望它平滑,请将其保留为设备默认值。
您正在渲染 24 点。除非您开始使用贝塞尔曲线(如我对您的其他问题的回答所考虑的那样),否则会产生非常粗大的输出。我会提高它(例如,在 iPhone 上,200 会产生相当平滑的波浪)。
在关闭路径之前,您缺少视图右下角的一条线。
您的波函数不太正确,返回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)需要注意的是该文件的CFAbsoluteTimeGetCurrent警告我们,
重复调用此函数并不能保证单调递增的结果。
我建议CACurrentMediaTime改用。
我建议dataPoints完全输掉。它没有提供任何价值。我只是继续dataPoint从视图的宽度计算。
我会坚持标准init(frame:)。这样,您既可以以编程方式添加视图,也可以直接在 Interface Builder 中添加它。
请记住使您的显示链接无效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)
这产生:
| 归档时间: |
|
| 查看次数: |
618 次 |
| 最近记录: |