如何在swift中制作圆形音频可视化工具?

我想制作一个像这个圆形可视化工具的可视化工具 ,点击绿色标志来查看动画。

在我的项目中,我首先绘制一个圆,我计算圆上的点来绘制可视化条,我旋转视图使条感觉像圆。 我使用StreamingKit来传输直播电台。 StreamingKit以分贝为单位提供实时音频功率。 然后我为可视化工具栏设置动画。 但是当我旋转视图时,高度和宽度会根据我旋转的角度而变化。 但边界值不会改变(我知道框架取决于superViews)。

audioSpectrom类

class audioSpectrom: UIView { let animateDuration = 0.15 let visualizerColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) var barsNumber = 0 let barWidth = 4 // width of bar let radius: CGFloat = 40 var radians = [CGFloat]() var barPoints = [CGPoint]() private var rectArray = [CustomView]() private var waveFormArray = [Int]() private var initialBarHeight: CGFloat = 0.0 private let mainLayer: CALayer = CALayer() // draw circle var midViewX: CGFloat! var midViewY: CGFloat! var circlePath = UIBezierPath() override init(frame: CGRect) { super.init(frame: frame) setupView() } convenience init() { self.init(frame: CGRect.zero) setupView() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setupView() } private func setupView() { self.layer.addSublayer(mainLayer) barsNumber = 10 } override func layoutSubviews() { mainLayer.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height) drawVisualizer() } //----------------------------------------------------------------- // MARK: - Drawing Section //----------------------------------------------------------------- func drawVisualizer() { midViewX = self.mainLayer.frame.midX midViewY = self.mainLayer.frame.midY // Draw Circle let arcCenter = CGPoint(x: midViewX, y: midViewY) let circlePath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true) let circleShapeLayer = CAShapeLayer() circleShapeLayer.path = circlePath.cgPath circleShapeLayer.fillColor = UIColor.blue.cgColor circleShapeLayer.strokeColor = UIColor.clear.cgColor circleShapeLayer.lineWidth = 1.0 mainLayer.addSublayer(circleShapeLayer) // Draw Bars rectArray = [CustomView]() for i in 0..<barsNumber { let angle = ((360 / barsNumber) * i) - 90 let point = calculatePoints(angle: angle, radius: radius) let radian = angle.degreesToRadians radians.append(radian) barPoints.append(point) let rectangle = CustomView(frame: CGRect(x: barPoints[i].x, y: barPoints[i].y, width: CGFloat(barWidth), height: CGFloat(barWidth))) initialBarHeight = CGFloat(self.barWidth) rectangle.setAnchorPoint(anchorPoint: CGPoint.zero) let rotationAngle = (CGFloat(( 360/barsNumber) * i)).degreesToRadians + 180.degreesToRadians rectangle.transform = CGAffineTransform(rotationAngle: rotationAngle) rectangle.backgroundColor = visualizerColor rectangle.layer.cornerRadius = CGFloat(rectangle.bounds.width / 2) rectangle.tag = i self.addSubview(rectangle) rectArray.append(rectangle) var values = [5, 10, 15, 10, 5, 1] waveFormArray = [Int]() var j: Int = 0 for _ in 0..<barsNumber { waveFormArray.append(values[j]) j += 1 if j == values.count { j = 0 } } } } //----------------------------------------------------------------- // MARK: - Animation Section //----------------------------------------------------------------- func animateAudioVisualizerWithChannel(level0: Float, level1: Float ) { DispatchQueue.main.async { UIView.animateKeyframes(withDuration: self.animateDuration, delay: 0, options: .beginFromCurrentState, animations: { for i in 0..<self.barsNumber { let channelValue: Int = Int(arc4random_uniform(2)) let wavePeak: Int = Int(arc4random_uniform(UInt32(self.waveFormArray[i]))) let barView = self.rectArray[i] as? CustomView guard var barFrame = barView?.frame else { return } // calculate the bar height let barH = (self.frame.height / 2 ) - self.radius // scale the value to 40, input value of this func range from 0-60, 60 is low and 0 is high. Then calculate the height by minimise the scaled height from bar height. let scaled0 = (CGFloat(level0) * barH) / 60 let scaled1 = (CGFloat(level1) * barH) / 60 let calc0 = barH - scaled0 let calc1 = barH - scaled1 if channelValue == 0 { barFrame.size.height = calc0 } else { barFrame.size.height = calc1 } if barFrame.size.height  ((self.frame.size.height / 2) - self.radius) { barFrame.size.height = self.initialBarHeight + CGFloat(wavePeak) } barView?.frame = barFrame } }, completion: nil) } } func calculatePoints(angle: Int, radius: CGFloat) -> CGPoint { let barX = midViewX + cos((angle).degreesToRadians) * radius let barY = midViewY + sin((angle).degreesToRadians) * radius return CGPoint(x: barX, y: barY) } } extension BinaryInteger { var degreesToRadians: CGFloat { return CGFloat(Int(self)) * .pi / 180 } } extension FloatingPoint { var degreesToRadians: Self { return self * .pi / 180 } var radiansToDegrees: Self { return self * 180 / .pi } } extension UIView{ func setAnchorPoint(anchorPoint: CGPoint) { var newPoint = CGPoint(x: self.bounds.size.width * anchorPoint.x, y: self.bounds.size.height * anchorPoint.y) var oldPoint = CGPoint(x: self.bounds.size.width * self.layer.anchorPoint.x, y: self.bounds.size.height * self.layer.anchorPoint.y) newPoint = newPoint.applying(self.transform) oldPoint = oldPoint.applying(self.transform) var position : CGPoint = self.layer.position position.x -= oldPoint.x position.x += newPoint.x; position.y -= oldPoint.y; position.y += newPoint.y; self.layer.position = position; self.layer.anchorPoint = anchorPoint; } } 

我将一个空视图拖到storyBoard并将自定义类作为audioSpectrom。

视图控制器

 func startAudioVisualizer() { visualizerTimer?.invalidate() visualizerTimer = nil visualizerTimer = Timer.scheduledTimer(timeInterval: visualizerAnimationDuration, target: self, selector: #selector(self.visualizerTimerFunc), userInfo: nil, repeats: true) } @objc func visualizerTimerFunc(_ timer: CADisplayLink) { let lowResults = self.audioPlayer!.averagePowerInDecibels(forChannel: 0) let lowResults1 = self.audioPlayer!.averagePowerInDecibels(forChannel: 1) audioSpectrom.animateAudioVisualizerWithChannel(level0: -lowResults, level1: -lowResults1) } 

OUTPUT

  1. 没有动画

在此处输入图像描述

  1. 随着动画

在此处输入图像描述

在我的观察中,旋转时框架的高度值和宽度值发生了变化。 当我将CGSize(width: 4, height: 4)赋予条形时,意味着当我使用某个角度旋转时,它会改变框架的大小,如CGSize(width: 3.563456, height: 5.67849) (不确定该值,它是一个假设)。

如何解决这个问题? 任何建议或答案将不胜感激。

编辑

 func animateAudioVisualizerWithChannel(level0: Float, level1: Float ) { DispatchQueue.main.async { UIView.animateKeyframes(withDuration: self.animateDuration, delay: 0, options: .beginFromCurrentState, animations: { for i in 0..<self.barsNumber { let channelValue: Int = Int(arc4random_uniform(2)) let wavePeak: Int = Int(arc4random_uniform(UInt32(self.waveFormArray[i]))) var barView = self.rectArray[i] as? CustomView guard let barViewUn = barView else { return } let barH = (self.frame.height / 2 ) - self.radius let scaled0 = (CGFloat(level0) * barH) / 60 let scaled1 = (CGFloat(level1) * barH) / 60 let calc0 = barH - scaled0 let calc1 = barH - scaled1 let kSavedTransform = barViewUn.transform barViewUn.transform = .identity if channelValue == 0 { barViewUn.frame.size.height = calc0 } else { barViewUn.frame.size.height = calc1 } if barViewUn.frame.height  ((self.frame.size.height / 2) - self.radius) { barViewUn.frame.size.height = self.initialBarHeight + CGFloat(wavePeak) } barViewUn.transform = kSavedTransform barView = barViewUn } }, completion: nil) } } 

产量

运行以下代码片段显示输出

  

您的代码中有两个(可能是三个)问题:


1. audioSpectrom.layoutSubviews()

您可以在layoutSubviews中创建新视图,并将它们添加到视图层次结构中。 这不是您想要做的事情,因为layoutSubviews被多次调用,您应该仅将其用于布局目的。 作为一个肮脏的解决方法,我修改了func drawVisualizer的代码,只添加了一次吧:

 func drawVisualizer() { // ... some code here // ... mainLayer.addSublayer(circleShapeLayer) // This will ensure to only add the bars once: guard rectArray.count == 0 else { return } // If we already have bars, just return // Draw Bars rectArray = [CustomView]() // ... Rest of the func } 

现在,它几乎看起来不错,但最顶部的杆仍有一些污垢效果。 所以你必须改变


2. audioSectrom.animateAudioVisualizerWithChannel(level0:level1:)

在这里,您想重新计算条形框架。 由于它们被旋转,框架也会旋转,你必须应用一些数学技巧。 为了避免这种情况,您可以更轻松地保存生活,保存旋转变换,将其设置为.identity ,修改框架,然后恢复原始旋转变换。 不幸的是,这会导致一些污垢效应,旋转为0或2pi,可能是由于一些圆角问题引起的。 没关系,有一个更简单的解决方案: 您可以更好地修改bounds ,而不是修改frame

  • frame在外部(在您的情况下:旋转)坐标系中测量
  • bounds在内部(非变形)坐标系中测量

所以我只是用函数animateAudioVisualizerWithChannel bounds替换了所有的frame ,并且还删除了转换矩阵的保存和恢复:

 func animateAudioVisualizerWithChannel(level0: Float, level1: Float ) { // some code before guard let barViewUn = barView else { return } let barH = (self.bounds.height / 2 ) - self.radius let scaled0 = (CGFloat(level0) * barH) / 60 let scaled1 = (CGFloat(level1) * barH) / 60 let calc0 = barH - scaled0 let calc1 = barH - scaled1 if channelValue == 0 { barViewUn.bounds.size.height = calc0 } else { barViewUn.bounds.size.height = calc1 } if barViewUn.bounds.height < CGFloat(4) || barViewUn.bounds.height > ((self.bounds.height / 2) - self.radius) { barViewUn.bounds.size.height = self.initialBarHeight + CGFloat(wavePeak) } barView = barViewUn // some code after } 

3.警告

顺便说一下,你应该摆脱代码中的所有警告。 我没有清理我的答案代码,以使其与原始代码相当。

例如,在var barView = self.rectArray[i] as? CustomView var barView = self.rectArray[i] as? CustomView您不需要条件转换,因为该数组已包含CustomView对象。 所以,所有barViewUn东西都是不必要的。 找到并清理的更多内容。