前言
由于項(xiàng)目需求中用到了帶有漸變色的能力雷達(dá)圖,而我們常用的一些三方控件并不能滿足我的項(xiàng)目需求。特此記錄一下自己的實(shí)現(xiàn)此功能的過程。主要使用UIBezierPath路徑相關(guān)、CAShapeLayer繪制相關(guān)、CAGradientLayer漸變色相關(guān),通過對(duì)上述三個(gè)類的組合來實(shí)現(xiàn)此功能。先上一下效果圖

雷達(dá)圖.png
最終的實(shí)現(xiàn)效果會(huì)有一點(diǎn)出入,主要是
背景顏色和文字顏色可能會(huì)有點(diǎn)不同。如有需要大家可以自行修改一下顏色就可以了
繪制前需要知道的一些東西
-
如何繪制正多邊形
繪制多邊形主要是獲取到對(duì)應(yīng)的點(diǎn)的坐標(biāo),下面是獲取點(diǎn)坐標(biāo)的方法
/*
//.pi 、 M_PI 、 Double.pi 這三個(gè)值是一樣的 只不過在OC和swift中的寫法有點(diǎn)不同
***繪制順序是逆時(shí)針順序***獲取橫坐標(biāo)***
index: 第幾個(gè)點(diǎn)的坐標(biāo)
ridus: 正多邊形的半徑值
count: 正幾邊形 如果傳入5的話就代表是5邊形
centerX:圓心的X坐標(biāo)
*/
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
}
/*
***繪制順序是逆時(shí)針順序***獲取縱坐標(biāo)***
index: 第幾個(gè)點(diǎn)的坐標(biāo)
ridus: 正多邊形的半徑值
count: 正幾邊形 如果傳入5的話就代表是5邊形
centerY:圓心的Y坐標(biāo)
*/
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
}
- 如何繪制一個(gè)帶邊線的不規(guī)則漸變五邊形(即本文章中最終實(shí)現(xiàn)的內(nèi)容視圖的樣式)
-
邊線的實(shí)現(xiàn)方法
在開發(fā)過程中我們肯定或多或少的都用過CAShapeLayer進(jìn)行一些繪制相關(guān)的操作,其中有一個(gè)設(shè)置畫筆顏色的方法strokeColor而這個(gè)屬性只能設(shè)置單顏色,所以在繪制漸變線條的時(shí)候這個(gè)方法是無法實(shí)現(xiàn)的,這時(shí)候就要用到CAGradientLayer這個(gè)與漸變有關(guān)的類來實(shí)現(xiàn)了。
下面來具體說一下實(shí)現(xiàn)思路:
第一步:設(shè)置CAGradientLayer的漸變色和對(duì)應(yīng)的frame
第二步:設(shè)置CAShapeLayer的fillColor和strokeColor
第三步:將設(shè)置好的CAShapeLayer添加到CAGradientLayer的mask屬性上面
第四步:將設(shè)置好的CAGradientLayer添加到view的layer上面
具體的關(guān)于fillColor和strokeColor
如果設(shè)置fillColor為clear則會(huì)只顯示線條部分的漸變色。這個(gè)就是實(shí)現(xiàn)線條漸變的核心的地方 -
內(nèi)容層漸變的實(shí)現(xiàn)方法
關(guān)于內(nèi)部層漸變的實(shí)現(xiàn)其實(shí)和邊線的實(shí)現(xiàn)差不多。主要的區(qū)別還是在于fillColor和strokeColor的設(shè)置上做一些不同的設(shè)置就可以了
繪制最里面的小五邊形
- 繪制路徑
private func setCenterCobWeb(){
//設(shè)置五邊形的五個(gè)頂點(diǎn)的位置
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),//此處我的圓心點(diǎn)是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)
}
//設(shè)置畫筆的相關(guān)屬性
let centerCobWebLayer = CAShapeLayer.init()
centerCobWebLayer.path = path.cgPath
centerCobWebLayer.lineWidth = 0//因?yàn)槲业膬?nèi)側(cè)的五邊形是帶有填充色的且線的顏色和填充色一樣所以此處設(shè)置0
//如果這個(gè)位置邊線和填充色不一樣的話則需要這樣設(shè)置
/*
centerCobWebLayer.fillColor = strokeColor.cgColor//自己替換顏色
centerCobWebLayer.strokeColor = strokeColor.cgColor//自己替換顏色
*/
centerCobWebLayer.fillColor = strokeColor.cgColor//此處修改成自己的填充色
//添加到layer上去
self.layer.addSublayer(centerCobWebLayer)
}
//獲取X坐標(biāo)
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坐標(biāo)
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
}
繪制最里面的小五邊形對(duì)應(yīng)的五條虛線的邊
//此方法可以直接在繪制小五邊形的時(shí)候在獲取坐標(biāo)點(diǎn)的for循環(huán)里面直接調(diào)用就可以了
private func setLinelayer(index : Int){
let lineLayer = CAShapeLayer.init()
lineLayer.bounds = self.bounds
//此處一定要設(shè)置,不然的話繪制的位置會(huì)出現(xiàn)變化
lineLayer.position = CGPoint.init(x: centerX, y: centerX) //定到你的圓心點(diǎn)的位置就可以了
lineLayer.fillColor = UIColor.clear.cgColor
lineLayer.strokeColor = strokeColor.cgColor
lineLayer.lineWidth = 1
lineLayer.lineDashPattern = [5,10]//虛線相關(guān)的屬性 【虛線長度,虛線間隔】
//開始點(diǎn)的坐標(biāo)-> 虛線離著最里面的小五邊形有一點(diǎn)點(diǎn)的距離。小五邊形的半徑是10 此處設(shè)置15,
//如果不需要也可以設(shè)置成和小五邊形一樣的半徑
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)
)
//結(jié)束點(diǎn)的坐標(biāo)->最外層的大的五邊形的坐標(biāo)
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 這個(gè)是因?yàn)椴粶p去這個(gè)寬度的話
//最終繪制出來的虛線和大五邊形的邊出現(xiàn)重疊的情況。如果沒有這個(gè)要求的話可以直接去掉這個(gè)
)
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)
}
//也可以最終不添加這個(gè)endPoind 直接調(diào)用 path.close()
//如果直接使用path.close() 呢在設(shè)置線條的圓角的話就會(huì)出現(xiàn)問題,
//如果線條的交點(diǎn)不需要圓角可以直接使用path.close()
path.addLine(to: endPoint)
let cobWebLayer = CAShapeLayer.init()
cobWebLayer.path = path.cgPath
cobWebLayer.lineWidth = CGFloat(cobwebLineWidth)//畫筆的寬度 自行修改
cobWebLayer.lineCap = .round//線條圓角相關(guān)
cobWebLayer.lineJoin = .round//線條圓角相關(guān)
cobWebLayer.strokeColor = strokeColor.cgColor//自行修改畫筆顏色
cobWebLayer.fillColor = Color.clear.cgColor//填充色要設(shè)置成透明的
self.layer.addSublayer(cobWebLayer)
}
繪制數(shù)據(jù)的填充層和線條
一定是要先繪制填充層
**關(guān)于代碼中pointXArray和pointYArray的說明:
- 在設(shè)置
gradientLayer的frame的時(shí)候設(shè)置多大呢么漸變的內(nèi)容層就是多大。所以正常的漸變應(yīng)該是內(nèi)容層呢一部分進(jìn)行漸變就可以了。如果設(shè)置的frame是當(dāng)前view的大小呢么內(nèi)容層的漸變只能是一部分。所以此處用兩個(gè)數(shù)組保存x坐標(biāo)和y坐標(biāo)用來回去最大值和最小值 - 設(shè)置了
gradientLayer的frame以后現(xiàn)在的數(shù)據(jù)層的坐標(biāo)的計(jì)算就類似于在一個(gè)View上添加一個(gè)ImagView的坐標(biāo)和你現(xiàn)在把這個(gè)ImageView添加到了一個(gè)新的View上面然后把新的View添加到當(dāng)前的view上面此時(shí)ImageView的坐標(biāo)性質(zhì)差不多。只要保證還是現(xiàn)實(shí)在原來的位置就可以了
private func setValueLayer(){
self.valueArray = [0.5,0.4,0.9,0.7,0.2]//五條邊對(duì)應(yīng)的占比
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//設(shè)置填充色為clear 則只會(huì)顯示線條部分的漸變色
lineChartLayer.lineWidth = 2//此處一定要設(shè)置線條的寬度
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)
}
大的五邊形的文字信息
關(guān)于大五邊形文字信息的設(shè)置這里就不上代碼了。簡單的說一下實(shí)現(xiàn)方法就可以了:
- 在我們繪制大五邊形的點(diǎn)的時(shí)候會(huì)獲取到五個(gè)點(diǎn),然后根據(jù)五個(gè)點(diǎn)的信息我們就可以拿到
label的frame的一些關(guān)鍵信息了。 - 根據(jù)獲取到的五個(gè)點(diǎn)正常的創(chuàng)建
Label然后添加到view上面就可以了
項(xiàng)目中我寫的完整的代碼
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:設(shè)置最內(nèi)側(cè)的正五邊形
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 = ["力量","恢復(fù)","耐力","柔韌性","平衡"]
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
}
}