iOS新增繪製圓的方法例項程式碼
iOS 的座標系和我們幾何課本中的二維座標系並不一樣!
# BezierPath繪製圓弧
使用 UIBezierPath 進行繪製圓弧的方法,通常會直接使用 addArc :
addArc(withCenter:,radius:,startAngle:,endAngle:,clockwise:)
或者使用 addCurve 進行擬圓弧:
addCurve(to:,controlPoint1:,controlPoint2:)
其實我們可以通過,兩個座標點(startPoint & endPoint),和兩點間的線段對應的圓弧的弧度(angle/radian)就能確定這個圓的資訊(半徑radius,center), 所以我們是不是可以封裝出只提供 start,end 和 angle 就能繪製 arc 的函式?
addArc(startPoint:,endPoint:,angle:,clockwise:)
# 計算兩點間的距離
這裡邏輯很簡單不做贅述。
func calculateLineLength(_ point1: CGPoint,_ point2: CGPoint) -> CGFloat { let w = point1.x - point2.x let h = point1.y - point2.y return sqrt(w * w + h * h) }
# 計算兩點間的夾角
計算 point 和 origin 連線在 iOS 座標系的角度
func calculateAngle(point: CGPoint,origin: CGPoint) -> Double { if point.y == origin.y { return point.x > origin.x ? 0.0 : -Double.pi } if point.x == origin.x { return point.y > origin.y ? Double.pi * 0.5 : Double.pi * -0.5 } // Note: 修正標準座標系角度到 iOS 座標系 let rotationAdjustment = Double.pi * 0.5 let offsetX = point.x - origin.x let offsetY = point.y - origin.y // Note: 使用 -offsetY 是因為 iOS 座標系與標準座標系的區別 if offsetY > 0 { return Double(atan(offsetX / -offsetY)) + rotationAdjustment } else { return Double(atan(offsetX / -offsetY)) - rotationAdjustment } }
# 計算圓心的座標
如果你已經將幾何知識丟的差不多了的話,我在這裡畫了個大概的草圖,如下( angle 比較小時):
angle 比較大時:
所以我麼可以寫出如下計算中心點的程式碼
// Woring: 只計算從start到end **順時針** 計算對應的 **小於π** 圓弧對應的圓心 // Note: 計算逆時針(end到start)可以看做將傳入的start和end對調後計算順時針時的圓心位置 // Note: 計算大於π的叫相當於將end和start對換後計算2π-angle的順時針圓心位置 // Note: 綜上傳入start,end,angle 右外部自行處理邏輯 func calculateCenterFor(startPoint start: CGPoint,endPoint end: CGPoint,radian: Double) -> CGPoint { guard radian <= Double.pi else { fatalError("Does not support radian calculations greater than π!") } guard start != end else { fatalError("Start position and end position cannot be equal!") } if radian == Double.pi { let centerX = (end.x - start.x) * 0.5 + start.x let centerY = (end.y - start.y) * 0.5 + start.y return CGPoint(x: centerX,y: centerY) } let lineAB = calculateLineLength(start,end) // 平行 Y 軸 if start.x == end.x { let centerY = (end.y - start.y) * 0.5 + start.y let tanResult = CGFloat(tan(radian * 0.5)) let offsetX = lineAB * 0.5 / tanResult let centerX = start.x + offsetX * (start.y > end.y ? 1.0 : -1.0) return CGPoint(x: centerX,y: centerY) } // 平行 X 軸 if start.y == end.y { let centerX = (end.x - start.x) * 0.5 + start.x let tanResult = CGFloat(tan(radian * 0.5)) let offsetY = lineAB * 0.5 / tanResult let centerY = start.y + offsetY * (start.x < end.x ? 1.0 : -1.0) return CGPoint(x: centerX,y: centerY) } // 普通情況 // 計算半徑 let radius = lineAB * 0.5 / CGFloat(sin(radian * 0.5)) // 計算與 Y 軸的夾角 let angleToYAxis = atan(abs(start.x - end.x) / abs(start.y - end.y)) let cacluteAngle = CGFloat(Double.pi - radian) * 0.5 - angleToYAxis // 偏移量 let offsetX = radius * sin(cacluteAngle) let offsetY = radius * cos(cacluteAngle) var centetX = end.x var centerY = end.y // 以 start 為原點判斷象限區間(iOS座標系) if end.x > start.x && end.y < start.y { // 第一象限 centetX = end.x + offsetX centerY = end.y + offsetY } else if end.x > start.x && end.y > start.y { // 第二象限 centetX = start.x - offsetX centerY = start.y + offsetY } else if end.x < start.x && end.y > start.y { // 第三象限 centetX = end.x - offsetX centerY = end.y - offsetY } else if end.x < start.x && end.y < start.y { // 第四象限 centetX = start.x + offsetX centerY = start.y - offsetY } return CGPoint(x: centetX,y: centerY) }
這裡附上一個逆時針繪製第一張圖中圓心位置的草圖,圖中已將 start 和 end 對換
如果你對其中計算時到底該使用 + 還是 - 有困惑的話也可以自己多畫些草圖大概驗證下,總之有疑惑多動手🤭
# 實現我們的目標函式
在有了計算圓心位置,和兩點間角度的函式後我們很容易就能實現 addArc(startPoint:,clockwise:) 了;
func addArc(startPoint start: CGPoint,angle: Double,clockwise: Bool) { guard start != end && (angle >= 0 && angle <= 2 * Double.pi) else { return } if angle == 0 { move(to: start) addLine(to: end) return } var tmpStart = start,tmpEnd = end,tmpAngle = angle // Note: 保證計算圓心時是從 start 到 end 順時針 小於 π 的角 if tmpAngle > Double.pi { tmpAngle = 2 * Double.pi - tmpAngle (tmpStart,tmpEnd) = (tmpEnd,tmpStart) } if !clockwise { (tmpStart,tmpStart) } let center = calculateCenterFor(startPoint: tmpStart,endPoint: tmpEnd,radian: tmpAngle) let radius = calculateLineLength(start,center) var startAngle = calculateAngle(point: start,origin: center) var endAngle = calculateAngle(point: end,origin: center) // Note: 逆時針繪製則交換 startAngle 和 endAngle,並且將開始點移動的 end 位置 if !clockwise { (startAngle,endAngle) = (endAngle,startAngle) move(to: end) } addArc(withCenter: center,radius: radius,startAngle: CGFloat(startAngle),endAngle: CGFloat(endAngle),clockwise: true) move(to: end) }
# 完結
最後也不知道是你否會碰到相同的需求,這裡附上原始碼和一份樣例及執行結果圖;
override func draw(_ rect: CGRect) { let path = UIBezierPath() var start = CGPoint(x: 160,y: 130) var end = CGPoint(x: 180,y: 200) path.move(to: start) path.addArc(startPoint: start,endPoint: end,angle: Double.pi * 1.6,clockwise: true) path.move(to: start) path.addArc(startPoint: start,angle: Double.pi * 0.8,clockwise: true) start = CGPoint(x: 142,y: 130) end = CGPoint(x: 162,angle: Double.pi * 0.4,clockwise: true) start = CGPoint(x: 140,y: 130) end = CGPoint(x: 160,clockwise: false) path.move(to: start) path.addArc(startPoint: start,clockwise: false) path.close() path.lineWidth = 1 UIColor.red.setStroke() path.stroke() }
ps: 每次都寫 Double.pi / x 很煩? 試試類似於 SwiftUI 提供的介面,使用 度數(degress) 而非 弧度(radian)
struct Angle { private var degress: Double static func deggess(_ degress: Double) -> Angle { return .init(degress: degress) } // 弧度 var radians: Double { Double.pi * degress / 180.0 } } // Angle.deggess(90).radians // 1.570796326794897
func addArc(startPoint start: CGPoint,angle: Angle,clockwise: Bool)
感謝閱讀,祝好祝順🥰
總結
到此這篇關於iOS新增繪製圓的文章就介紹到這了,更多相關iOS新增繪製圓內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!