前言
由于项目需求中用到了带有渐变色的能力雷达图,而我们常用的一些三方控件并不能满足我的项目需求。特此记录一下自己的实现此功能的过程。主要使用UIBezierPath
路径相关、CAShapeLayer
绘制相关、CAGradientLayer
渐变色相关,通过对上述三个类的组合来实现此功能。先上一下效果图
最终的实现效果会有一点出入,主要是
背景颜色
和文字颜色
可能会有点不同。如有需要大家可以自行修改一下颜色就可以了
绘制前需要知道的一些东西
- 如何绘制正多边形
绘制多边形主要是获取到对应的点的坐标,下面是获取点坐标的方法
/*
//.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
}
- 如何绘制一个带边线的不规则渐变五边形(即本文章中最终实现的内容视图的样式)
-
边线的实现方法
在开发过程中我们肯定或多或少的都用过CAShapeLayer
进行一些绘制相关的操作,其中有一个设置画笔颜色的方法strokeColor
而这个属性只能设置单颜色,所以在绘制渐变线条的时候这个方法是无法实现的,这时候就要用到CAGradientLayer
这个与渐变有关的类来实现了。
下面来具体说一下实现思路:
第一步:设置CAGradientLayer
的渐变色和对应的frame
第二步:设置CAShapeLayer
的fillColor
和strokeColor
第三步:将设置好的CAShapeLayer
添加到CAGradientLayer
的mask
属性上面
第四步:将设置好的CAGradientLayer
添加到view
的layer
上面
具体的关于fillColor
和strokeColor
如果设置fillColor
为clear
则会只显示线条部分的渐变色。这个就是实现线条渐变的核心的地方 -
内容层渐变的实现方法
关于内部层渐变的实现其实和边线的实现差不多。主要的区别还是在于fillColor
和strokeColor
的设置上做一些不同的设置就可以了
绘制最里面的小五边形
- 绘制路径
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)
}
绘制数据的填充层和线条
一定是要先绘制填充层
**关于代码中pointXArray
和pointYArray
的说明:
- 在设置
gradientLayer
的frame
的时候设置多大呢么渐变的内容层就是多大。所以正常的渐变应该是内容层呢一部分进行渐变就可以了。如果设置的frame
是当前view的大小呢么内容层的渐变只能是一部分。所以此处用两个数组保存x坐标
和y坐标
用来回去最大值和最小值 - 设置了
gradientLayer
的frame
以后现在的数据层的坐标的计算就类似于在一个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)
}
大的五边形的文字信息
关于大五边形文字信息的设置这里就不上代码了。简单的说一下实现方法就可以了:
- 在我们绘制大五边形的点的时候会获取到五个点,然后根据五个点的信息我们就可以拿到
label
的frame
的一些关键信息了。 - 根据获取到的五个点正常的创建
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
}
}