Swift绘制雷达图(蛛网图、五方图)

前言

由于项目需求中用到了带有渐变色的能力雷达图,而我们常用的一些三方控件并不能满足我的项目需求。特此记录一下自己的实现此功能的过程。主要使用UIBezierPath路径相关、CAShapeLayer绘制相关、CAGradientLayer渐变色相关,通过对上述三个类的组合来实现此功能。先上一下效果图

雷达图.png

最终的实现效果会有一点出入,主要是背景颜色文字颜色可能会有点不同。如有需要大家可以自行修改一下颜色就可以了

绘制前需要知道的一些东西

  • 如何绘制正多边形
    绘制多边形主要是获取到对应的点的坐标,下面是获取点坐标的方法
/*
//.pi 、 M_PI  、 Double.pi   这三个值是一样的  只不过在OC和swift中的写法有点不同


***绘制顺序是逆时针顺序***获取横坐标***
index: 第几个点的坐标
ridus: 正多边形的半径值
count: 正几边形  如果传入5的话就代表是5边形
centerX:圆心的X坐标
*/
    private func pointX(index : Int , ridus : Double , count : Int , centerX : Double) -> Double{
        return centerX - cos(.pi / 180 * (90.0 - 360.0 / count * Double(index))) * ridus
    }
    
/*
***绘制顺序是逆时针顺序***获取纵坐标***
index: 第几个点的坐标
ridus: 正多边形的半径值
count: 正几边形  如果传入5的话就代表是5边形
centerY:圆心的Y坐标
*/
    private func pointY(index : Int , ridus : Double , count : Int , centerY : Double) -> Double{
        return centerY - sin(.pi / 180 * (90.0 - 360.0 / count * Double(index))) * ridus
    }
  • 如何绘制一个带边线的不规则渐变五边形(即本文章中最终实现的内容视图的样式)
  1. 边线的实现方法
    在开发过程中我们肯定或多或少的都用过CAShapeLayer进行一些绘制相关的操作,其中有一个设置画笔颜色的方法strokeColor而这个属性只能设置单颜色,所以在绘制渐变线条的时候这个方法是无法实现的,这时候就要用到CAGradientLayer这个与渐变有关的类来实现了。
    下面来具体说一下实现思路:
    第一步:设置CAGradientLayer的渐变色和对应的frame
    第二步:设置CAShapeLayerfillColorstrokeColor
    第三步:将设置好的CAShapeLayer添加到CAGradientLayermask属性上面
    第四步:将设置好的CAGradientLayer添加到viewlayer上面
    具体的关于fillColorstrokeColor
    如果设置fillColorclear则会只显示线条部分的渐变色。这个就是实现线条渐变的核心的地方
  2. 内容层渐变的实现方法
    关于内部层渐变的实现其实和边线的实现差不多。主要的区别还是在于fillColorstrokeColor的设置上做一些不同的设置就可以了

绘制最里面的小五边形

  1. 绘制路径
    private func setCenterCobWeb(){
        //设置五边形的五个顶点的位置
        let path = UIBezierPath.init()
        for index in 0...4 {
            let point = CGPoint.init(
                x: pointX(index : index , ridus : 10 , count : 5 , centerY : self.center.x),//此处我的圆心点是View的正中心
                y: pointY(index : index , ridus : 10 , count : 5 , centerY : self.center.x)
            )
            if index == 0 {
                path.move(to: point)
                continue
            }
            path.addLine(to: point)
        }
        
        //设置画笔的相关属性
        let centerCobWebLayer = CAShapeLayer.init()
        centerCobWebLayer.path = path.cgPath
        centerCobWebLayer.lineWidth = 0//因为我的内侧的五边形是带有填充色的且线的颜色和填充色一样所以此处设置0
        //如果这个位置边线和填充色不一样的话则需要这样设置
        /*
          centerCobWebLayer.fillColor = strokeColor.cgColor//自己替换颜色
          centerCobWebLayer.strokeColor = strokeColor.cgColor//自己替换颜色
        */
        centerCobWebLayer.fillColor = strokeColor.cgColor//此处修改成自己的填充色
        //添加到layer上去
        self.layer.addSublayer(centerCobWebLayer)
        
    }
     //获取X坐标
     private func pointX(index : Int , ridus : Double , count : Int , centerX : Double) -> Double{
        return centerX - cos(.pi / 180 * (90.0 - 360.0 / count * Double(index))) * ridus
    }
    //获取Y坐标
    private func pointY(index : Int , ridus : Double , count : Int , centerY : Double) -> Double{
        return centerY - sin(.pi / 180 * (90.0 - 360.0 / count * Double(index))) * ridus
    }

绘制最里面的小五边形对应的五条虚线的边

    //此方法可以直接在绘制小五边形的时候在获取坐标点的for循环里面直接调用就可以了
    private func setLinelayer(index : Int){
        let lineLayer = CAShapeLayer.init()
        lineLayer.bounds = self.bounds
        //此处一定要设置,不然的话绘制的位置会出现变化
        lineLayer.position = CGPoint.init(x: centerX, y: centerX)  //定到你的圆心点的位置就可以了
        lineLayer.fillColor = UIColor.clear.cgColor
        lineLayer.strokeColor = strokeColor.cgColor
        lineLayer.lineWidth = 1
        lineLayer.lineDashPattern = [5,10]//虚线相关的属性  【虚线长度,虚线间隔】
        //开始点的坐标-> 虚线离着最里面的小五边形有一点点的距离。小五边形的半径是10  此处设置15,
        //如果不需要也可以设置成和小五边形一样的半径
        let startPoint = CGPoint.init(
            x: pointX(index : index , ridus : 15 , count : 5 , centerY : self.center.x),
            y: pointY(index : index , ridus : 15 , count : 5 , centerY : self.center.x)
        )
        //结束点的坐标->最外层的大的五边形的坐标
        let endPoint = CGPoint.init(
            x: pointX(index : index , ridus : 大五边形的半径 - 大五边形的线的宽度 * 2 , count : 5 , centerY : self.center.x),
            y: pointY(index : index , ridus : 大五边形的半径 - 大五边形的线的宽度 * 2 , count : 5 , centerY : self.center.x)
            //说明一下:大五边形的线的宽度 * 2  这个是因为不减去这个宽度的话
            //最终绘制出来的虚线和大五边形的边出现重叠的情况。如果没有这个要求的话可以直接去掉这个
        )
        
        let path = CGMutablePath.init()
        path.move(to: startPoint)
        path.addLine(to: endPoint)
        lineLayer.path = path
        self.layer.addSublayer(lineLayer)
    }

绘制大五边形

    private func setCobwebLineLayer(){
        let path = UIBezierPath.init()
        var endPoint = CGPoint.init(x: 0, y: 0)
        for index in 0...4 {//绘制几边形就到几
            let point = CGPoint.init(
                
                x: pointX(index : index , ridus : 大五边形半径 , count : 5 , centerY : self.center.x),
                y: pointY(index : index , ridus : 大五边形半径 , count : 5 , centerY : self.center.x)
            )
            if index == 0 {
                path.move(to: point)
                endPoint = point
                continue
            }
            path.addLine(to: point)
        }
        //也可以最终不添加这个endPoind 直接调用 path.close()
        //如果直接使用path.close() 呢在设置线条的圆角的话就会出现问题,
        //如果线条的交点不需要圆角可以直接使用path.close()
        path.addLine(to: endPoint)
        
        let cobWebLayer = CAShapeLayer.init()
        cobWebLayer.path = path.cgPath
        cobWebLayer.lineWidth = CGFloat(cobwebLineWidth)//画笔的宽度 自行修改
        cobWebLayer.lineCap = .round//线条圆角相关
        cobWebLayer.lineJoin = .round//线条圆角相关
        cobWebLayer.strokeColor = strokeColor.cgColor//自行修改画笔颜色
        cobWebLayer.fillColor = Color.clear.cgColor//填充色要设置成透明的
        self.layer.addSublayer(cobWebLayer)
    }

绘制数据的填充层和线条

一定是要先绘制填充层
**关于代码中pointXArraypointYArray的说明:

  1. 在设置gradientLayerframe的时候设置多大呢么渐变的内容层就是多大。所以正常的渐变应该是内容层呢一部分进行渐变就可以了。如果设置的frame是当前view的大小呢么内容层的渐变只能是一部分。所以此处用两个数组保存x坐标y坐标用来回去最大值和最小值
  2. 设置了gradientLayerframe以后现在的数据层的坐标的计算就类似于在一个View上添加一个ImagView的坐标和你现在把这个ImageView添加到了一个新的View上面然后把新的View添加到当前的view上面此时ImageView的坐标性质差不多。只要保证还是现实在原来的位置就可以了
    private func setValueLayer(){
        self.valueArray = [0.5,0.4,0.9,0.7,0.2]//五条边对应的占比
        let path = UIBezierPath.init()
        var pointXArray = [CGFloat]()
        var pointYArray = [CGFloat]()
        for (index , value) in self.valueArray!.enumerated() {
            let point = CGPoint.init(
                x: pointX(index : index , ridus : 15 + (ridus - 15) * value , count : 5 , centerY : self.center.x),
                y: pointY(index : index , ridus : 15 + (ridus - 15) * value , count : 5 , centerY : self.center.x)
            )
            pointXArray.append(point.x)
            pointYArray.append(point.y)
        }
        for (index , value) in pointXArray.enumerated() {
            if index == 0 {
                path.move(to: CGPoint.init(x: value - pointXArray.min()!, y: pointYArray[index] - pointYArray.min()!))
            }else{
                path.addLine(to: CGPoint.init(x: value - pointXArray.min()!, y: pointYArray[index] - pointYArray.min()!))
            }
        }
        let shapeLayer = CAShapeLayer.init()
        shapeLayer.path = path.cgPath
        shapeLayer.strokeColor = UIColor.white.cgColor
        shapeLayer.fillColor = UIColor.white.cgColor
        shapeLayer.lineWidth = 0//此处做填充层 所以没有线 画笔宽度为0就可以了
        
        let gradientLayer = CAGradientLayer.init()
        gradientLayer.frame = CGRect(x: pointXArray.min()!, y: pointYArray.min()!, width: pointXArray.max()! - pointXArray.min()!, height: pointYArray.max()! - pointYArray.min()!)
        gradientLayer.startPoint = CGPoint.init(x: 0.5, y: 0)
        gradientLayer.endPoint = CGPoint.init(x: 0.5, y: 1)
        let gradientLayerColors = infoFillColors
        gradientLayer.colors = gradientLayerColors
        gradientLayer.mask = shapeLayer
        self.layer.addSublayer(gradientLayer)
    }

绘制渐变的线条

private func setValueLineLayer(){
        let path = UIBezierPath.init()
        var pointXArray = [CGFloat]()
        var pointYArray = [CGFloat]()
        for (index , value) in self.valueArray!.enumerated() {
            let point = CGPoint.init(
                 x: pointX(index : index , ridus : 15 + (ridus - 15) * value , count : 5 , centerY : self.center.x),
                y: pointY(index : index , ridus : 15 + (ridus - 15) * value , count : 5 , centerY : self.center.x)
            )
            pointXArray.append(point.x)
            pointYArray.append(point.y)
        }
        
        for (index , value) in pointXArray.enumerated() {
            if index == 0 {
                path.move(to: CGPoint.init(x: value - pointXArray.min()!, y: pointYArray[index] - pointYArray.min()!))
            }else{
                path.addLine(to: CGPoint.init(x: value - pointXArray.min()!, y: pointYArray[index] - pointYArray.min()!))
            }
        }
        
        path.close()
        let lineChartLayer = CAShapeLayer.init()
        lineChartLayer.path = path.cgPath
        lineChartLayer.strokeColor = UIColor.white.cgColor
        lineChartLayer.fillColor = UIColor.clear.cgColor//设置填充色为clear 则只会显示线条部分的渐变色
        lineChartLayer.lineWidth = 2//此处一定要设置线条的宽度
        let gradientLayer =  CAGradientLayer.init()
        gradientLayer.frame = CGRect(x: pointXArray.min()!, y: pointYArray.min()!, width:pointXArray.max()! - pointXArray.min()!, height: pointYArray.max()! - pointYArray.min()!)
        gradientLayer.colors = infoLineColors
        gradientLayer.startPoint = CGPoint.init(x:0.5, y:0);
        gradientLayer.endPoint = CGPoint.init(x:0.5,y: 1);
        gradientLayer.mask = lineChartLayer
        self.layer.addSublayer(gradientLayer)
    }

大的五边形的文字信息

关于大五边形文字信息的设置这里就不上代码了。简单的说一下实现方法就可以了:

  • 在我们绘制大五边形的点的时候会获取到五个点,然后根据五个点的信息我们就可以拿到labelframe的一些关键信息了。
  • 根据获取到的五个点正常的创建Label然后添加到view上面就可以了

项目中我写的完整的代码

import UIKit

class CobwebChartView: UIView {
    
    var centerX : Double = 0
    var centerY : Double = 0
    var ridus : Double = 0
    let cobwebLineWidth : Double = 2
    let strokeColor = UIColor.init(red: 30 / 255.0, green: 174 / 255.0, blue: 197 / 255.0, alpha: 1)
    let infoFillColors = [UIColor.init(red: 248 / 255.0, green: 24 / 255.0, blue: 101 / 255.0, alpha: 0.16).cgColor,
                          UIColor.init(red: 157 / 255.0, green:109 / 255.0, blue: 211 / 255.0, alpha: 0.16).cgColor]
    //let infoFillColors = [UIColor.red.cgColor,UIColor.blue.cgColor]
    let infoLineColors = [UIColor.init(red: 248 / 255.0, green: 24 / 255.0, blue: 101 / 255.0, alpha: 1).cgColor,
                          UIColor.init(red: 157 / 255.0, green:109 / 255.0, blue: 211 / 255.0, alpha: 1).cgColor]
    
    var valueArray : [Double]?{
        didSet{
            self.setCenterCobWeb()
            self.setCobwebLineLayer()
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = Color.viewBgColor
        self.centerX = Double(self.height / 2.0)
        self.centerY = Double(self.height / 2.0)
        self.ridus = centerX - 50
        self.setCenterCobWeb()
        self.setCobwebLineLayer()
        self.setValueLayer()
        self.setValueLineLayer()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

extension CobwebChartView{
    //MARK:设置最内侧的正五边形
    private func setCenterCobWeb(){
        let path = UIBezierPath.init()
        for index in 0...4 {
            let point = CGPoint.init(
                x: pointX(index: index, ridus: 10),
                y: pointY(index: index, ridus: 10)
            )
            setLinelayer(index: index)
            if index == 0 {
                path.move(to: point)
                continue
            }
            path.addLine(to: point)
        }
        
        let centerCobWebLayer = CAShapeLayer.init()
        centerCobWebLayer.path = path.cgPath
        centerCobWebLayer.lineWidth = 0
        centerCobWebLayer.fillColor = strokeColor.cgColor
        self.layer.addSublayer(centerCobWebLayer)
        
    }
    
    private func setLinelayer(index : Int){
        let lineLayer = CAShapeLayer.init()
        lineLayer.bounds = self.bounds
        lineLayer.position = CGPoint.init(x: centerX, y: centerX)
        lineLayer.fillColor = UIColor.clear.cgColor
        lineLayer.strokeColor = strokeColor.cgColor
        lineLayer.lineWidth = 1
        lineLayer.lineDashPattern = [5,10]
        let startPoint = CGPoint.init(
            x: pointX(index: index, ridus: 15),
            y: pointY(index: index, ridus: 15)
        )
        
        let endPoint = CGPoint.init(
            x: pointX(index: index, ridus: ridus - cobwebLineWidth * 2),
            y: pointY(index: index, ridus: ridus - cobwebLineWidth * 2)
        )
        
        let path = CGMutablePath.init()
        path.move(to: startPoint)
        path.addLine(to: endPoint)
        lineLayer.path = path
        self.layer.addSublayer(lineLayer)
    }
    
    private func setCobwebLineLayer(){
        let path = UIBezierPath.init()
        var endPoint = CGPoint.init(x: 0, y: 0)
        let titleArray = ["力量","恢复","耐力","柔韧性","平衡"]
        for index in 0...4 {
            let point = CGPoint.init(
                x: pointX(index: index, ridus: ridus),
                y: pointY(index: index, ridus: ridus)
            )
            
            if index == 0 {
                path.move(to: point)
                endPoint = point
                setTitleLabel(position: .top, point: point, title: titleArray[index])
                continue
            }
            if index == 1 {
                setTitleLabel(position: .left, point: point, title: titleArray[index])
            }
            
            if index == 2 || index == 3 {
                setTitleLabel(position: .bottom, point: point, title: titleArray[index])
            }
            if index == 4 {
                setTitleLabel(position: .right, point: point, title: titleArray[index])
            }
            path.addLine(to: point)
        }
        path.addLine(to: endPoint)
        
        let cobWebLayer = CAShapeLayer.init()
        cobWebLayer.path = path.cgPath
        cobWebLayer.lineWidth = CGFloat(cobwebLineWidth)
        cobWebLayer.lineCap = .round
        cobWebLayer.lineJoin = .round
        cobWebLayer.strokeColor = strokeColor.cgColor
        cobWebLayer.fillColor = Color.clear.cgColor
        self.layer.addSublayer(cobWebLayer)
    }
    
    private func setValueLayer(){
        self.valueArray = [0.5,0.4,0.9,0.7,0.2]
        let path = UIBezierPath.init()
        var pointXArray = [CGFloat]()
        var pointYArray = [CGFloat]()
        for (index , value) in self.valueArray!.enumerated() {
            let point = CGPoint.init(
                x: pointX(index: index, ridus: 15 + (ridus - 15) * value),
                y: pointY(index: index, ridus: 15 + (ridus - 15) * value)
            )
            pointXArray.append(point.x)
            pointYArray.append(point.y)
        }
        for (index , value) in pointXArray.enumerated() {
            if index == 0 {
                path.move(to: CGPoint.init(x: value - pointXArray.min()!, y: pointYArray[index] - pointYArray.min()!))
            }else{
                path.addLine(to: CGPoint.init(x: value - pointXArray.min()!, y: pointYArray[index] - pointYArray.min()!))
            }
        }
        let shapeLayer = CAShapeLayer.init()
        shapeLayer.path = path.cgPath
        shapeLayer.strokeColor = UIColor.white.cgColor
        shapeLayer.fillColor = UIColor.white.cgColor
        shapeLayer.lineWidth = 0
        
        let gradientLayer = CAGradientLayer.init()
        gradientLayer.frame = CGRect(x: pointXArray.min()!, y: pointYArray.min()!, width: pointXArray.max()! - pointXArray.min()!, height: pointYArray.max()! - pointYArray.min()!)
        gradientLayer.startPoint = CGPoint.init(x: 0.5, y: 0)
        gradientLayer.endPoint = CGPoint.init(x: 0.5, y: 1)
        let gradientLayerColors = infoFillColors
        gradientLayer.colors = gradientLayerColors
        gradientLayer.mask = shapeLayer
        self.layer.addSublayer(gradientLayer)
        
        
        
    }
    
    private func setValueLineLayer(){
        let path = UIBezierPath.init()
        var pointXArray = [CGFloat]()
        var pointYArray = [CGFloat]()
        for (index , value) in self.valueArray!.enumerated() {
            let point = CGPoint.init(
                x: pointX(index: index, ridus: 15 + (ridus - 15) * value),
                y: pointY(index: index, ridus: 15 + (ridus - 15) * value)
            )
            pointXArray.append(point.x)
            pointYArray.append(point.y)
        }
        
        for (index , value) in pointXArray.enumerated() {
            if index == 0 {
                path.move(to: CGPoint.init(x: value - pointXArray.min()!, y: pointYArray[index] - pointYArray.min()!))
            }else{
                path.addLine(to: CGPoint.init(x: value - pointXArray.min()!, y: pointYArray[index] - pointYArray.min()!))
            }
        }
        
        path.close()
        let lineChartLayer = CAShapeLayer.init()
        lineChartLayer.path = path.cgPath
        lineChartLayer.strokeColor = UIColor.white.cgColor
        lineChartLayer.fillColor = UIColor.clear.cgColor
        lineChartLayer.lineWidth = 2
        let gradientLayer =  CAGradientLayer.init()
        gradientLayer.frame = CGRect(x: pointXArray.min()!, y: pointYArray.min()!, width:pointXArray.max()! - pointXArray.min()!, height: pointYArray.max()! - pointYArray.min()!)
        gradientLayer.colors = infoLineColors
        gradientLayer.startPoint = CGPoint.init(x:0.5, y:0);
        gradientLayer.endPoint = CGPoint.init(x:0.5,y: 1);
        gradientLayer.mask = lineChartLayer
        self.layer.addSublayer(gradientLayer)
    }
    
    private func setTitleLabel(position : LabelPosition , point : CGPoint , title : String){
        let titleLabel = UILabel.init()
        titleLabel.text = title
        titleLabel.font = UIFont.systemFont(ofSize: 14)
        titleLabel.sizeToFit()
        if position == .left {
            titleLabel.frame = CGRect(x: point.x - 10 - titleLabel.frame.size.width, y: point.y - titleLabel.frame.size.height / 2.0, width: titleLabel.frame.size.width, height: titleLabel.frame.size.height)
        }
        if position == .top {
            titleLabel.frame = CGRect(x: point.x - titleLabel.frame.size.width / 2.0, y: point.y - titleLabel.frame.size.height - 10, width: titleLabel.frame.size.width, height: titleLabel.frame.size.height)
        }
        
        if position == .right {
            titleLabel.frame = CGRect(x: point.x + 10, y: point.y - titleLabel.frame.size.height / 2.0, width: titleLabel.frame.size.width, height: titleLabel.frame.size.height)
        }
        if position == .bottom {
            titleLabel.frame = CGRect(x: point.x - titleLabel.frame.size.width / 2.0, y: point.y + 10, width: titleLabel.frame.size.width, height: titleLabel.frame.size.height)
        }
        self.addSubview(titleLabel)
    }
    
    enum LabelPosition {
        case left
        case right
        case top
        case bottom
    }
}

extension CobwebChartView{
    private func pointX(index : Int , ridus : Double) -> Double{
        return centerX - cos(.pi / 180 * (90.0 - 360.0 / 5 * Double(index))) * ridus
    }
    
    private func pointY(index : Int , ridus : Double) -> Double{
        return centerY - sin(.pi / 180 * (90.0 - 360.0 / 5 * Double(index))) * ridus
    }
}

你可能感兴趣的:(Swift绘制雷达图(蛛网图、五方图))