添加新项目时为 CAShapelayer 内容设置动画

Posted

技术标签:

【中文标题】添加新项目时为 CAShapelayer 内容设置动画【英文标题】:Animate CAShapelayer content when new item is added 【发布时间】:2021-05-23 12:58:18 【问题描述】:

我需要像 ios 语音备忘录应用一样渲染移动的音频波形。在这里,我维护波形:[Int] rms 波幅。 现在,当新的波浪出现时,将其添加到波形[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

【问题讨论】:

【参考方案1】:

不要尝试在draw(rect:) 中制作动画。这将所有工作都放在 CPU 上,而不是 GPU 上,并且没有利用 iOS 中的硬件加速动画。

我建议改为使用CAShapeLayerCABasicAnimation 来为您的路径设置动画。

CGPathUIBezierPath 安装到CAShapeLayer,然后创建一个CABasicAnimation 来更改形状层的路径属性。

获得平滑形状动画的诀窍是动画中的每一步都有相同数量的控制点。因此,您不应在路径中添加越来越多的点,而应创建一个包含波形最后 n 点图形的新路径。

我建议保留您要绘制的 n 个点的环形缓冲区,并在该环形缓冲区之外构建一个 GCPath/UIBezierPath。当您添加更多点时,较旧的点会“老化”出环形缓冲区,并且您将始终绘制相同数量的点。

编辑:

好吧,你需要比环形缓冲区更简单的东西:我们称它为 lastNElementsBuffer。它应该允许您添加项目,丢弃最旧的元素,然后始终返回最近添加的元素。

这是一个简单的实现:

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  $0 
    

如果您创建这样一个 CGFloat 值缓冲区,并用全零填充它,那么您可以在读取新波形值时开始将它们保存到其中。

然后您将创建一个动画,该动画将使用值缓冲区和一个新值创建一个路径,然后创建一个将新路径向左移动的动画,从而显示新点。

我在 Github 上创建了一个演示项目来展示该技术。你可以在这里下载:https://github.com/DuncanMC/LastNItemsBufferGraph.git

这是一个示例动画:

编辑#2:

听起来您需要的动画风格与我在示例应用中所做的略有不同。您应该修改 GraphView.swift 中的方法 buildPath(plusValue:) 以从样本值数组(加上一个可选的新值)中绘制您想要的图形样式。示例应用程序的其余部分应该可以正常工作。

我更新了应用程序以提供类似于 Apple 语音备忘录应用程序的条形图样式:

编辑#3:

在another thread 中,您说您希望能够允许用户在图表中来回滚动,并建议使用滚动视图来管理该过程。

问题在于您的音频样本可能会超过很长的时间间隔,因此整个波形图的图像可能太大而无法保存在内存中。 (想象一个 3 分钟的记录图,每 1/20 秒有一个数据点,每个数据点的图形是 10 点宽乘 200 点高。那是 3 * 60 * 20 = 3600 个数据点。如果您在 3X Retina 显示器上水平使用 10 个点,即每个数据点 30 像素宽或 108,000 像素宽, • 200 点 • 3X = 600 像素高,或 6480 万像素。在 3 字节/像素,(8 位/颜色没有 alpha),这是 1.944 亿字节的数据,仅用于 3 分钟的录制。现在假设这是 2 小时的录制。是时候耗尽内存并崩溃了。)

相反,我会说您应该为整个记录保存一个数据点缓冲区,按比例缩小到可以为您提供一个点精度的最小数据类型。您可能可以为每个数据点使用一个字节。将这些点保存在一个结构中,该结构还包括样本/秒。

编写一个函数,将图形数据结构和时间偏移量作为输入,并为该时间偏移量生成一个 CGPath,加上或减去足够的数据点以使图形比两侧的显示窗口更宽。然后,您可以在任一方向上为图形设置动画以进行正向或反向播放。您可以实现一个轻击手势识别器,让用户来回拖动图形、滑块或任何您需要的东西。当您到达当前图表的末尾时,您只需调用您的函数来生成图表的新部分并将该新部分偏移显示到正确的屏幕位置。

【讨论】:

感谢 Dunac 的意见。假设我在 CAShapelayer 的右侧添加了新的 UIBezierPath 行,并且在图层上添加了四个 UIBezierPath 行,所以如果在右侧添加新的 UIBezierPath 行,那么在添加之前我需要在左侧移动现有的四行..所以我担心的是我我没有得到有效的方法来翻译同一个 CAShapeLayer 上现有的 UIBezierPath 行并在右侧添加新行.. 不,不要在右侧添加新的UIBezierPath 点。丢弃 Bezier 路径并为每个动画步骤重新构建它。因为旧值“老化”而新值从右侧进入,所以图形将从右到左平滑地动画。请参阅我的答案的编辑。 假设我的缓冲区最大大小为 5,那么您的意思是如果有新条目出现,那么我应该从父层中删除 CAShapeLayer 并创建新的 CAShapelayer,并在其上添加最新的五个 UBezierPath 吗?或者您能否提供更多关于动画点的解释?我是 Core Animation 的新手,所以可能无法完全理解你.. 我的方法不像描述的那样工作得很好。让我考虑一下。 好的,我写了一个示例项目来说明如何制作这种动画并将其放在 Github 上。查看我的答案的修改。

以上是关于添加新项目时为 CAShapelayer 内容设置动画的主要内容,如果未能解决你的问题,请参考以下文章

将 CAShapeLayer 添加到 UIButton 不起作用

最大 CAShapeLayer 大小?

在子视图中添加/删除视图时为视图设置动画

粒子效果 QQ粘性布局 (CAShapeLayer形状图层)

CAShapeLayer 动画路径毛刺/闪烁(从椭圆到矩形并返回)

用 CAShapeLayer 添加阴影?