前言
要實(shí)現(xiàn)如圖片中左側(cè)的正六邊形按鈕魁蒜,其中要有邊框以及角的弧度囊扳。由于以前做過CALayer
相關(guān)的功能,自然想起利用CALayer
繪制path
來實(shí)現(xiàn)該功能兜看。
根據(jù)最大半徑計算各頂點(diǎn)坐標(biāo)
先確定按鈕的size
得出最大r
值锥咸,然后按照這個模式得出每個點(diǎn)相對于按鈕的坐標(biāo),使用UIBezierPath
繪制path
得到最后的圖樣细移。按照這樣的邏輯確實(shí)可以做出如UI展示的效果(先忽略圓角和邊框)搏予,但是因此會引發(fā)一個問題:這種代碼是一種死代碼。本著代碼的可擴(kuò)展性原則弧轧,在此封裝了一個多邊形按鈕
弧度計算頂點(diǎn)坐標(biāo)
- 首先進(jìn)行一下邏輯分析雪侥,按照固定點(diǎn)坐標(biāo)的做法會導(dǎo)致代碼死板,因此換個思路進(jìn)行設(shè)計: 使用
角度
的形式來設(shè)置對應(yīng)點(diǎn)坐標(biāo)精绎。 - 以按鈕的中心點(diǎn)
centerPoint
作為原點(diǎn)速缨,按鈕所在坐標(biāo)系的x
軸上點(diǎn)(r, 0)
點(diǎn)作為繪制的起始點(diǎn),依據(jù)順時針方向依次計算每個點(diǎn)的坐標(biāo) - 根據(jù)規(guī)律可知對應(yīng)弧度所表示的點(diǎn)的坐標(biāo)的
x
值等于centerPoint.x + r * cos(弧度)
代乃,而y
值等于centerPoint.y + r * sin(弧度)
該坐標(biāo)既是多邊形對應(yīng)頂點(diǎn)的坐標(biāo)
獲取單個點(diǎn)的坐標(biāo)
private func vertexCoordinates(radius: CGFloat, angle: Double, offset: Double = 0) ->CGPoint {
let centerPoint = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
let X = centerPoint.x + radius * (angle + offset).cosValue
let Y = centerPoint.y + radius * (angle + offset).sinValue
return CGPoint(x: X, y: Y)
}
根據(jù)多邊形的邊數(shù)旬牲、最大半徑以及偏移弧度獲得多邊形的每個頂點(diǎn)的坐標(biāo)
private func regularPolygonCoordinates(sides: Int, radius: CGFloat, offset: Double = 0) ->[CGPoint] {
assert(sides >= 3, "多邊形最少為3邊")
assert(radius > 0, "多邊形半徑必須大于0")
var coordinates = [CGPoint]()
for i in 0..<sides {
let corner = Double(360) / Double(sides)
let radian = corner / Double(180) * Double.pi
let radianOfPoint = Double(i) * radian
let point = vertexCoordinates(radius: radius, angle: radianOfPoint, offset: offset)
coordinates.append(point)
}
return coordinates
}
再進(jìn)一步根據(jù)獲得的各個頂點(diǎn)的坐標(biāo)數(shù)組可以用UIBezierPath
繪制圖案
let points = regularPolygonCoordinates(sides: style.sides, radius: r, offset: style.offset)
for (index, point) in points.enumerated() {
if index == 0 {
path.move(to: point)
}else {
path.addLine(to: point)
}
}
path.close()
到現(xiàn)在只是得到了完整的path
還需要設(shè)置到layer
上才能變相的按該路徑裁剪按鈕
private func hexagon() {
var Max_R: CGFloat
if style.Max_Radius == 0 {
let W = bounds.width
let H = bounds.height
assert(W > 0 && H > 0, "此時寬或者高沒有值")
Max_R = H > W ? W / 2 : H / 2
}else {
Max_R = style.Max_Radius
}
assert(Max_R > style.borderWidth, "多邊形最大半徑不能小于邊界寬度")
for (index, layer) in self.layer.sublayers!.enumerated() {
if index != 0 {
layer.removeFromSuperlayer()
}
}
let topLayer = CAShapeLayer()
topLayer.path = drawPath(radius: Max_R)
topLayer.strokeColor = style.borderColor.cgColor
topLayer.fillColor = UIColor.clear.cgColor
topLayer.lineWidth = style.borderWidth
let bottomLayer = CAShapeLayer()
bottomLayer.path = drawPath(radius: Max_R)
self.layer.mask = bottomLayer
self.layer.insertSublayer(topLayer, above: bottomLayer)
}
然后在layoutSubviews()
方法中調(diào)用該繪制方法即可獲得對應(yīng)圖形的按鈕
override func layoutSubviews() {
super.layoutSubviews()
hexagon()
}
圓角的設(shè)置
我這里的圓角采用的不是平時按鈕的cornerRadius
,而是采用的貝塞爾曲線
的形式設(shè)置圓角搁吓,即根據(jù)兩個定點(diǎn)以及一個控制點(diǎn)來繪制一條有弧度的曲線原茅,原因是多邊形如果設(shè)置cornerRadius
會導(dǎo)致曲率很大最后裁剪出的弧度十分難看(不認(rèn)同的話歡迎指正)
如果把正多邊形
以中心點(diǎn)
、頂點(diǎn)
與鄰近頂點(diǎn)
組成的等邊三角形
視為一個子模塊堕仔,選取貝塞爾曲線
的固定兩點(diǎn)其一為D
點(diǎn)(另一點(diǎn)以AC
邊對稱)與控制點(diǎn)C
擂橘,則只需要計算出D
點(diǎn)對應(yīng)的坐標(biāo)即可(C
點(diǎn)坐標(biāo)已知,r
最大半徑已知贮预,CD
長度自定義贝室,多邊形邊數(shù)自定義)
- 首先需要計算出
∠DAC
角的弧度 - 然后計算出
AD
邊的大小 - 根據(jù)普通正多邊形各頂點(diǎn)坐標(biāo)的規(guī)律計算出
AD
為R
的多邊形相對于原多邊形偏移∠DAC
角度的各頂點(diǎn)坐標(biāo)
private func regularPolygonCoordinatesWithRoundedCorner(sides: Int, radius: CGFloat, offset: Double = 0) ->[CGPoint] {
assert(sides >= 3, "多邊形最少為3邊")
assert(radius > 0, "多邊形半徑必須大于0")
let CAB = Double(360) / Double(sides) / Double(180) * Double.pi
let EC = Double(radius * (CAB / 2).sinValue)
let AE = Double(radius * (CAB / 2).cosValue)
let ED = EC - style.filletDegree
let EAD = atan(ED / AE)
let DAC = CAB / 2 - EAD
let newRadius = sqrt(pow(AE, 2) + pow(ED, 2))
var coordinates = [CGPoint]()
for i in 0..<sides {
let direction = Double(i) * Double(360) / Double(sides) / Double(180) * Double.pi
let point = vertexCoordinates(radius: radius, angle: direction, offset: offset)
let leftAngle = direction - DAC
let leftPoint = vertexCoordinates(radius: CGFloat(newRadius), angle: leftAngle, offset: offset)
let rightAngle = direction + DAC
let rightPoint = vertexCoordinates(radius: CGFloat(newRadius), angle: rightAngle, offset: offset)
coordinates.append(leftPoint)
coordinates.append(point)
coordinates.append(rightPoint)
}
return coordinates
}
通過上面的代碼邏輯可以計算出有圓角的多邊形的各關(guān)鍵點(diǎn)坐標(biāo)契讲,然后就只需要把這些關(guān)鍵點(diǎn)根據(jù)規(guī)律連接到一起。
let points = regularPolygonCoordinatesWithRoundedCorner(sides: style.sides, radius: r, offset: style.offset)
var temPoint: CGPoint!
for (index, point) in points.enumerated() {
if index == 0 {
path.move(to: point)
}else {
let remainder = index % 3
switch remainder {
case 0:
path.addLine(to: point)
case 1:
temPoint = point
case 2:
path.addQuadCurve(to: point, controlPoint: temPoint)
default:
break
}
}
}
path.close()
封裝控制屬性
class SYPolygonStyle {
/// 是否可以圓角
var roundedCornersEnable: Bool = false
/// 圓角程度 - 值越大,角越平滑
var filletDegree: Double = 5.0
/// 邊界寬度(邊線的寬度)
var borderWidth: CGFloat = 0.0
/// 邊界顏色
var borderColor: UIColor = UIColor.gray
/// 多邊形最大半徑 -- 如果不設(shè)置該值, 默認(rèn)是按鈕中可顯示的最大多邊形的半徑
var Max_Radius: CGFloat = 0.0
/// 整個路徑以按鈕的中心點(diǎn)為中心按順時針方向偏移的弧度(需要傳入一個帶π的弧度) -- 默認(rèn)六邊形頂點(diǎn)為水平方向, 如果設(shè)置該值為π/2則頂點(diǎn)為豎直方向
var offset: Double = 0
/// 多邊形的邊數(shù) - 默認(rèn)是正六邊形
var sides: Int = 6
}
當(dāng)然滑频,有了控制屬性的類沒有使用場景怎么行捡偏,只需要一個新的構(gòu)造方法即可
init(frame: CGRect = CGRect.zero, style: SYPolygonStyle = SYPolygonStyle()) {
self.style = style
super.init(frame: frame)
}
使用方法類似于下例:
let style = SYPolygonStyle()
style.filletDegree = 10
style.borderWidth = 10
style.borderColor = .orange
style.roundedCornersEnable = true
style.offset = Double.pi / 4
style.sides = 5
let liubianxing = SYPolygonButton(frame: CGRect.zero, style: style)
liubianxing.setImage(UIImage.init(named: "zhbd_qq_icon"), for: .normal)
liubianxing.setImage(UIImage.init(named: "zhbd_weibo_icon"), for: .selected)
liubianxing.addTarget(self, action: #selector(btnClickAction(sender:)), for: .touchUpInside)
view.addSubview(liubianxing)
liubianxing.snp.makeConstraints { (make) in
make.top.left.equalTo(100)
make.width.height.equalTo(100)
}