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

时间:2018-01-06 07:12:36

标签: ios swift uiviewanimation cgaffinetransform

我想制作一个像Circular visualizer这样的可视化工具,点击绿色标记即可看到动画。

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

audioSpectrom Class

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 < 4 || 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。

的ViewController

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

输出

  1. 没有动画
  2. enter image description here

    1. 动画
    2. enter image description here

      在我的观察中,旋转时帧的高度值和宽度值发生了变化。意味着当我将CGSize(width: 4, height: 4)提供给bar时,然后当我使用某个角度旋转时,它会改变帧的大小,如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 < CGFloat(4) || 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)
              }
          }
      

      输出

      运行以下代码段显示输出

      &#13;
      &#13;
      <a href="https://imgflip.com/gif/227xsa"><img src="https://i.imgflip.com/227xsa.gif" title="made at imgflip.com"/></a>
      &#13;
      &#13;
      &#13;

      GOT IT !!

      circular-visualizer

1 个答案:

答案 0 :(得分:3)

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

<强> 1。 audioSpectrom.layoutSubviews()

您可以在layoutSubviews中创建新视图,并将它们添加到视图层次结构中。这不是您打算做的事情,因为layoutSubviews被多次调用,您应该仅将其用于布局目的。 作为一种肮脏的解决方法,我修改了函数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,可能是由于某些圆角问题引起的。没关系,有一个更简单的解决方案: 您最好修改frame

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

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

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中,您不需要条件强制转换,因为该数组已包含CustomView个对象。 因此,所有barViewUn内容都是不必要的。 找到并清理更多。