添加新项目时动画 CAShapelayer 内容
Animate CAShapelayer content when new item is added
我需要渲染移动的音频波形,例如 iOS 语音备忘录应用。在这里,我保持波形:[Int] rms 波幅。
现在,当新的波浪出现时,将它添加到 waveform[Int] 中,我在 CAShapeLayer
的右侧添加新的 UIBezierPath
行,并将整个 CAShapeLayer
平移 5 个点。
但是翻译动画不是那么流畅。你能建议更好的方法吗?
我当前的实现:
override func draw(_ rect: CGRect) {
shiftWaveform()
let path: UIBezierPath!
if let ppath = caLayer.path {
path = UIBezierPath(cgPath: ppath)
} else {
path = UIBezierPath()
}
guard var wave = waveforms.last else { return }
if (Int(wave) <= 2) {
wave = 2
}
count += 1
let startX = Int(self.bounds.width) + 5*count
let startY = Int(self.bounds.origin.y) + Int(self.bounds.height)/2
path.move(to: CGPoint(x: startX, y: startY + min(wave, Int(bounds.height/2))))
path.addLine(to: CGPoint(x: startX, y: startY - min(wave, Int(bounds.height/2))))
caLayer.path = path.cgPath
}
不要尝试在 draw(rect:)
中制作动画。这将所有工作都放在 CPU 上,而不是 GPU,并且没有利用 iOS 中的硬件加速动画。
我建议改为使用 CAShapeLayer
和 CABasicAnimation
来设置路径动画。
将 UIBezierPath
中的 CGPath
安装到 CAShapeLayer
中,然后创建一个 CABasicAnimation
来更改形状层的路径 属性。
获得平滑形状动画的诀窍是为动画中的每个步骤设置相同数量的控制点。因此,您不应向路径中添加越来越多的点,而应创建一个包含波形最后 n 个点的图形的新路径。
我建议保留要绘制的 n 个点的环形缓冲区,并从该环形缓冲区构建 GCPath/UIBezierPath。当您添加更多点时,较旧的点会从环形缓冲区中“老化”,并且您始终绘制相同数量的点。
编辑:
好的,您需要比环形缓冲区更简单的东西:我们称它为 lastNElementsBuffer。它应该让你添加项目,丢弃最旧的元素,然后总是 return 添加最新的元素。
这是一个简单的实现:
public struct LastNItemsBuffer<T> {
fileprivate var array: [T?]
fileprivate var index = 0
public init(count: Int) {
array = [T?](repeating: nil, count: count)
}
public mutating func clear() {
forceToValue(value: nil)
}
public mutating func forceToValue(value: T?) {
let count = array.count
array = [T?](repeating: value, count: count)
}
public mutating func write(_ element: T) {
array[index % array.count] = element
index += 1
}
public func lastNItems() -> [T] {
var result = [T?]()
for loop in 0..<array.count {
result.append(array[(loop+index) % array.count])
}
return result.compactMap { [=10=] }
}
}
如果您创建这样一个 CGFloat 值的缓冲区,并用全零填充它,您就可以在读取新波形值时开始将它们保存到其中。
然后您将创建一个动画,该动画将使用值的缓冲区加上一个新值创建一条路径,然后创建一个将新路径向左移动的动画,显示新点。
我在 Github 上创建了一个展示该技术的演示项目。您可以在这里下载:https://github.com/DuncanMC/LastNItemsBufferGraph.git
这是一个示例动画:
编辑#2:
听起来您需要的动画风格与我在示例应用程序中所做的略有不同。您应该修改 GraphView.swift
中的方法 buildPath(plusValue:)
以从样本值数组(加上一个可选的新值)中绘制您想要的图形样式。示例应用程序的其余部分应按编写的方式工作。
我更新了应用程序以提供类似于 Apple 语音备忘录应用程序的条形图样式:
编辑#3:
在 中,您说您希望能够允许用户在图表中来回滚动,并建议使用滚动视图来管理该过程。
问题在于您的音频样本可能会持续很长时间,因此整个波形图的图像可能太大而无法保存在内存中。 (想象一个 3 分钟记录的图表,每 1/20 秒有一个数据点,每个数据点被绘制为 10 点宽 x 200 点高。那就是 3 * 60 * 20 = 3600 个数据点。如果您在 3X 视网膜显示屏上水平使用 10 个点,即每个数据点 30 像素宽或 108,000 像素宽, • 200 点 • 3X = 600 像素高,或 6480 万像素。在 3 bytes/pixel,(8 bits/color 没有 alpha)这是 1.944 亿字节的数据,仅用于 3 分钟的记录。现在假设它是 2 小时长的记录。是时候 运行 内存不足和崩溃了。)
相反,我会说你应该为你的整个记录保存一个数据点缓冲区,按比例缩小到可以给你一个点精度的最小数据类型。您可能会为每个数据点使用一个字节。将这些点保存在还包含 samples/second.
的结构中
编写一个函数,将图形数据结构和时间偏移量作为输入,并为该时间偏移量生成一个 CGPath,加上或减去足够的数据点,使图形比显示宽 window边。然后您可以在任一方向上为图形设置动画以进行正向或反向播放。您可以实现一个点击手势识别器,让用户来回拖动图形、滑块或任何您需要的东西。当您到达当前图表的末尾时,您只需调用您的函数来生成图表的新部分并将新部分偏移显示到正确的屏幕位置。
我需要渲染移动的音频波形,例如 iOS 语音备忘录应用。在这里,我保持波形:[Int] rms 波幅。
现在,当新的波浪出现时,将它添加到 waveform[Int] 中,我在 CAShapeLayer
的右侧添加新的 UIBezierPath
行,并将整个 CAShapeLayer
平移 5 个点。
但是翻译动画不是那么流畅。你能建议更好的方法吗?
我当前的实现:
override func draw(_ rect: CGRect) {
shiftWaveform()
let path: UIBezierPath!
if let ppath = caLayer.path {
path = UIBezierPath(cgPath: ppath)
} else {
path = UIBezierPath()
}
guard var wave = waveforms.last else { return }
if (Int(wave) <= 2) {
wave = 2
}
count += 1
let startX = Int(self.bounds.width) + 5*count
let startY = Int(self.bounds.origin.y) + Int(self.bounds.height)/2
path.move(to: CGPoint(x: startX, y: startY + min(wave, Int(bounds.height/2))))
path.addLine(to: CGPoint(x: startX, y: startY - min(wave, Int(bounds.height/2))))
caLayer.path = path.cgPath
}
不要尝试在 draw(rect:)
中制作动画。这将所有工作都放在 CPU 上,而不是 GPU,并且没有利用 iOS 中的硬件加速动画。
我建议改为使用 CAShapeLayer
和 CABasicAnimation
来设置路径动画。
将 UIBezierPath
中的 CGPath
安装到 CAShapeLayer
中,然后创建一个 CABasicAnimation
来更改形状层的路径 属性。
获得平滑形状动画的诀窍是为动画中的每个步骤设置相同数量的控制点。因此,您不应向路径中添加越来越多的点,而应创建一个包含波形最后 n 个点的图形的新路径。
我建议保留要绘制的 n 个点的环形缓冲区,并从该环形缓冲区构建 GCPath/UIBezierPath。当您添加更多点时,较旧的点会从环形缓冲区中“老化”,并且您始终绘制相同数量的点。
编辑:
好的,您需要比环形缓冲区更简单的东西:我们称它为 lastNElementsBuffer。它应该让你添加项目,丢弃最旧的元素,然后总是 return 添加最新的元素。
这是一个简单的实现:
public struct LastNItemsBuffer<T> {
fileprivate var array: [T?]
fileprivate var index = 0
public init(count: Int) {
array = [T?](repeating: nil, count: count)
}
public mutating func clear() {
forceToValue(value: nil)
}
public mutating func forceToValue(value: T?) {
let count = array.count
array = [T?](repeating: value, count: count)
}
public mutating func write(_ element: T) {
array[index % array.count] = element
index += 1
}
public func lastNItems() -> [T] {
var result = [T?]()
for loop in 0..<array.count {
result.append(array[(loop+index) % array.count])
}
return result.compactMap { [=10=] }
}
}
如果您创建这样一个 CGFloat 值的缓冲区,并用全零填充它,您就可以在读取新波形值时开始将它们保存到其中。
然后您将创建一个动画,该动画将使用值的缓冲区加上一个新值创建一条路径,然后创建一个将新路径向左移动的动画,显示新点。
我在 Github 上创建了一个展示该技术的演示项目。您可以在这里下载:https://github.com/DuncanMC/LastNItemsBufferGraph.git
这是一个示例动画:
编辑#2:
听起来您需要的动画风格与我在示例应用程序中所做的略有不同。您应该修改 GraphView.swift
中的方法 buildPath(plusValue:)
以从样本值数组(加上一个可选的新值)中绘制您想要的图形样式。示例应用程序的其余部分应按编写的方式工作。
我更新了应用程序以提供类似于 Apple 语音备忘录应用程序的条形图样式:
编辑#3:
在
问题在于您的音频样本可能会持续很长时间,因此整个波形图的图像可能太大而无法保存在内存中。 (想象一个 3 分钟记录的图表,每 1/20 秒有一个数据点,每个数据点被绘制为 10 点宽 x 200 点高。那就是 3 * 60 * 20 = 3600 个数据点。如果您在 3X 视网膜显示屏上水平使用 10 个点,即每个数据点 30 像素宽或 108,000 像素宽, • 200 点 • 3X = 600 像素高,或 6480 万像素。在 3 bytes/pixel,(8 bits/color 没有 alpha)这是 1.944 亿字节的数据,仅用于 3 分钟的记录。现在假设它是 2 小时长的记录。是时候 运行 内存不足和崩溃了。)
相反,我会说你应该为你的整个记录保存一个数据点缓冲区,按比例缩小到可以给你一个点精度的最小数据类型。您可能会为每个数据点使用一个字节。将这些点保存在还包含 samples/second.
的结构中编写一个函数,将图形数据结构和时间偏移量作为输入,并为该时间偏移量生成一个 CGPath,加上或减去足够的数据点,使图形比显示宽 window边。然后您可以在任一方向上为图形设置动画以进行正向或反向播放。您可以实现一个点击手势识别器,让用户来回拖动图形、滑块或任何您需要的东西。当您到达当前图表的末尾时,您只需调用您的函数来生成图表的新部分并将新部分偏移显示到正确的屏幕位置。