
目錄
- 曲線上點(diǎn)算法
- 點(diǎn)的手勢處理:點(diǎn)擊線生成點(diǎn)和距離線有效距離內(nèi)拖動(dòng)生成點(diǎn)
- 方格和曲線交叉點(diǎn)的坐標(biāo)獲取
一、曲線上點(diǎn)算法
1.1、思路:根據(jù)坐標(biāo)上的點(diǎn),計(jì)算控制點(diǎn),從而再通過控制點(diǎn)之間分割成100個(gè)點(diǎn)(具體的控制點(diǎn)之間點(diǎn)的個(gè)數(shù)可以自己定義),通過每個(gè)控制點(diǎn)之間的計(jì)算生成點(diǎn),最后串起來就是上圖看到的曲線
-
1.2、部分代碼
//MARK: 通過已知點(diǎn)繪制path private func calculate(pointList: [CGPoint]) { allPointList.removeAll() let path = CGMutablePath() // 曲線斜率 let sharpenRatio = 1.0 if (pointList.count < 3) { path.addLines(between: pointList) drawPath(path: path) return } var pMidOfLm = CGPoint() var pMidOfMr = CGPoint() var cache: CGPoint? = nil var startPoint = pointList[0] for i in 0...pointList.count - 3 { let pL = pointList[I] let pM = pointList[i + 1] let pR = pointList[i + 2] pMidOfLm.x = (pL.x + pM.x) / 2.0 pMidOfLm.y = (pL.y + pM.y) / 2.0 pMidOfMr.x = (pM.x + pR.x) / 2.0 pMidOfMr.y = (pM.y + pR.y) / 2.0 let lengthOfLm = distanceBetweenPoints(pL, pM) let lengthOfMr = distanceBetweenPoints(pR, pM) var ratio = lengthOfLm / (lengthOfLm + lengthOfMr) * sharpenRatio let oneMinusRatio = (1 - ratio) * sharpenRatio let dx = pMidOfLm.x - pMidOfMr.x let dy = pMidOfLm.y - pMidOfMr.y var cLeft = CGPoint() cLeft.x = pM.x + dx * ratio cLeft.y = pM.y + dy * ratio var cRight = CGPoint() cRight.x = pM.x + -dx * oneMinusRatio cRight.y = pM.y + -dy * oneMinusRatio if (i == 0) { let pMidOfLCLeft = CGPoint(x: (pL.x + cLeft.x) / 2.0, y: (pL.y + cLeft.y) / 2.0) let pMidOfCLeftM = CGPoint(x: (cLeft.x + pM.x) / 2.0, y: (cLeft.y + pM.y) / 2.0) let length1 = distanceBetweenPoints(cLeft, pL) let length2 = distanceBetweenPoints(cLeft, pM) ratio = length1 / (length1 + length2) * sharpenRatio var first = CGPoint() first.x = cLeft.x + (pMidOfLCLeft.x - pMidOfCLeftM.x) * ratio first.y = cLeft.y + (pMidOfLCLeft.y - pMidOfCLeftM.y) * ratio addPoint(startPoint, first, cLeft, pM) startPoint = pM } else { // bezierPath.move(to: startPoint) if let weakCache = cache { // bezierPath.addCurve(to: pM, control1: weakCache, control2: cLeft) addPoint(startPoint, weakCache, cLeft, pM) startPoint = pM } } cache = cRight if (i == pointList.count - 3) { let pMidOfMCRight = CGPoint(x: (pM.x + cRight.x) / 2.0, y: (pM.y + cRight.y) / 2.0) let pMidOfCRightR = CGPoint(x: (pR.x + cRight.x) / 2.0, y: (pR.y + cRight.y) / 2.0) let length1 = distanceBetweenPoints(cRight, pM) let length2 = distanceBetweenPoints(pR, cRight) ratio = length2 / (length1 + length2) * sharpenRatio var last = CGPoint() last.x = cRight.x + (pMidOfCRightR.x - pMidOfMCRight.x) * ratio last.y = cRight.y + (pMidOfCRightR.y - pMidOfMCRight.y) * ratio // startPoint = pM // bezierPath.move(to: startPoint) // bezierPath.addCurve(to: pR, control1: cRight, control2: last) addPoint(startPoint, cRight, last, pR) } } path.addLines(between: allPointList) drawPath(path: path) }
二、點(diǎn)的手勢處理
-
2.1、點(diǎn)擊線生成點(diǎn)
點(diǎn)擊線生成點(diǎn)分析:點(diǎn)擊線的話,首先是拿到點(diǎn)擊點(diǎn)的坐標(biāo)p0,根據(jù)這個(gè)坐標(biāo),獲取這個(gè)點(diǎn)與線垂直和水平交叉點(diǎn)的坐標(biāo)p1和p2,看這個(gè)兩個(gè)點(diǎn)到p0距離
代碼示例://MARK: 父視圖點(diǎn)擊手勢 /// 父視圖點(diǎn)擊手勢 /// - Parameter panGesture: 手勢 @objc func superTapGester(gesture: UITapGestureRecognizer) { guard let currentPath, isCanUserInteractionEnabled, points.count < maxCircleViewNumber else { return } let tapLocation = gesture.location(in: self) debugPrint("Tap location in parent view: \(tapLocation)") // 1、點(diǎn)擊點(diǎn)首先要在 左右兩個(gè)點(diǎn)的矩形內(nèi),如果不在不生點(diǎn) var previousPoint: CGPoint = CGPoint() var nextPoint: CGPoint = CGPoint() /// 要插入的index var insertIndex: Int = 0 for (index, item) in points.enumerated() { if tapLocation.x < item.x { insertIndex = index // 找到后面的點(diǎn) nextPoint = item break } previousPoint = item } guard tapLocation.x > previousPoint.x && tapLocation.y < previousPoint.y && tapLocation.x < nextPoint.x && tapLocation.y > nextPoint.y else { debugPrint("?不在矩形范圍內(nèi)", "previousPoint:\(previousPoint) nextPoint:\(nextPoint)") return } // 在矩形的范圍內(nèi),確定添加的點(diǎn)事垂直點(diǎn)還是水平點(diǎn) // 垂直點(diǎn) let vPoint = getPointXY(xy: tapLocation.x, path: currentPath) // 水平點(diǎn) let hPoint = getPointXY(xy: tapLocation.y, path: currentPath, isX: false) // 垂直長度 let vLength: CGFloat = abs(tapLocation.y - vPoint.y) // 水平長度 let hLength: CGFloat = abs(tapLocation.x - hPoint.x) guard vLength < effectiveDistance || hLength < effectiveDistance else { debugPrint("?在矩形范圍內(nèi) ?:不在有效距離:\(effectiveDistance) 內(nèi), 垂直距離:\(vLength) 水平距離:\(hLength)") return } // 在有效的范圍內(nèi) var point: CGPoint = CGPoint() if vLength < hLength { point = vPoint debugPrint("?在矩形范圍內(nèi):取值垂直的點(diǎn)") } else { point = hPoint debugPrint("?在矩形范圍內(nèi):取值水平的點(diǎn)") } // 2、在矩形內(nèi),生成一個(gè)點(diǎn) let view = CircleView() view.layer.cornerRadius = 7.5 view.clipsToBounds = false view.backgroundColor = .randomColor view.layer.borderWidth = 3 view.layer.borderColor = UIColor.white.cgColor view.tag = insertIndex + 100 self.addSubview(view) // 插入視圖 circleViews.insert(view, at: insertIndex) // 插入生成的點(diǎn) points.insert(point, at: insertIndex) // 改變其他視圖的tag for index in (insertIndex + 1)...(circleViews.count - 1) { circleViews[index].tag = index + 100 } let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGester)) view.addGestureRecognizer(panGestureRecognizer) view.snp.makeConstraints { make in make.center.equalTo(point) make.size.equalTo(CGSize(width: 15, height: 15)) } // 加震動(dòng) let generator = UINotificationFeedbackGenerator() generator.notificationOccurred(.success) let param = getParamPointArray() dataClosure?(param.cmd_state, param.auxiliary_curve) } -
2.2、距離線有效距離內(nèi)拖動(dòng)生成點(diǎn)
分析:拖動(dòng)的話,首先是拿到點(diǎn)拖動(dòng)起點(diǎn)的坐標(biāo)p0,根據(jù)p0的坐標(biāo)xy,分別獲取曲線上的點(diǎn)p1和p2,同2.1一樣拿到p0與兩點(diǎn)的距離,看是否在有效距離effectiveDistance內(nèi),如果在看誰距離很近,近的則是點(diǎn)生成點(diǎn)的起點(diǎn),從而拖動(dòng)中點(diǎn)跟著移動(dòng),這個(gè)是利用的父視圖的拖動(dòng)手勢
代碼示例://MARK: 父視圖拖動(dòng)手勢 /// 父視圖拖動(dòng)手勢 /// - Parameter panGesture: 手勢 @objc func superPanGester(panGesture: UIPanGestureRecognizer) { // 最多maxCircleViewNumber個(gè)點(diǎn),包含兩頭的點(diǎn) guard isCanUserInteractionEnabled, points.count < maxCircleViewNumber else { return } switch panGesture.state { case .began: let startPanLocation = panGesture.location(in: self) let result = isPointLine(point: startPanLocation) if result.isEffectivePoint { // 在拖動(dòng)開始的位置生成一個(gè)點(diǎn) superPanInserTag = 100 + result.insertIndex // 2、在矩形內(nèi),生成一個(gè)點(diǎn) let view = CircleView() view.layer.cornerRadius = 7.5 view.clipsToBounds = false view.backgroundColor = .randomColor view.layer.borderWidth = 3 view.layer.borderColor = UIColor.white.cgColor view.tag = superPanInserTag self.addSubview(view) // 插入視圖 circleViews.insert(view, at: result.insertIndex) // 插入生成的點(diǎn) points.insert(startPanLocation, at: result.insertIndex) // 改變其他視圖的tag for index in (result.insertIndex + 1)...(circleViews.count - 1) { circleViews[index].tag = index + 100 } let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panGester)) view.addGestureRecognizer(panGestureRecognizer) view.snp.makeConstraints { make in make.center.equalTo(startPanLocation) make.size.equalTo(CGSize(width: 15, height: 15)) } setNeedsDisplay() // 加震動(dòng) let generator = UINotificationFeedbackGenerator() generator.notificationOccurred(.success) } debugPrint("super-拖動(dòng)開始: \(startPanLocation) superPanInserTag:\(superPanInserTag)") case .changed: let tapLocation = panGesture.location(in: self) if superPanInserTag > 0 { debugPrint("super-拖動(dòng)中: \(tapLocation) inserTag:\(superPanInserTag)") // 添加的點(diǎn)跟著移動(dòng) let panGestureRecognizerTag = superPanInserTag - 100 let previousPoint: CGPoint = points[panGestureRecognizerTag - 1] let nextPoint: CGPoint = points[panGestureRecognizerTag + 1] guard tapLocation.x > previousPoint.x && tapLocation.y < previousPoint.y && tapLocation.x < nextPoint.x && tapLocation.y > nextPoint.y else { debugPrint("?不在矩形范圍內(nèi)", "previousPoint:\(previousPoint) nextPoint:\(nextPoint)") // 移除該點(diǎn) points.remove(at: panGestureRecognizerTag) let view = circleViews[panGestureRecognizerTag] circleViews.remove(at: panGestureRecognizerTag) view.removeFromSuperview() for index in panGestureRecognizerTag...(circleViews.count - 1) { circleViews[index].tag = index + 100 } // 加震動(dòng) let generator = UINotificationFeedbackGenerator() generator.notificationOccurred(.success) superPanInserTag = 0 setNeedsDisplay() return } let view = circleViews[panGestureRecognizerTag] view.snp.updateConstraints { make in make.center.equalTo(tapLocation) } points[panGestureRecognizerTag] = tapLocation debugPrint("打印tag:\(panGestureRecognizerTag)") setNeedsDisplay() } case .ended: superPanInserTag = 0 debugPrint("super-拖動(dòng)結(jié)束 新的value") default: debugPrint("super-其他") } } //MARK: 是否響應(yīng)父視圖拖動(dòng)的手勢 /// 是否響應(yīng)拖動(dòng)的手勢:實(shí)現(xiàn) gestureRecognizer(_:shouldReceive:) 方法 func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { // 根據(jù)條件決定是否響應(yīng)手勢 if isCanUserInteractionEnabled { let location = touch.location(in: self) let result = isPointLine(point: location) return result.isEffectivePoint } else { return false } }
三、方格和曲線交叉點(diǎn)的坐標(biāo)獲取
-
3.1、方格
這個(gè)花方格就比較簡單了,只需要兩個(gè)for循環(huán)即可class GridView: UIView { override init(frame: CGRect) { super.init(frame: frame) } override func draw(_ rect: CGRect) { super.draw(rect) let width: CGFloat = rect.size.width let height: CGFloat = rect.size.height // 創(chuàng)建一個(gè)UIBezierPath對(duì)象 let path = UIBezierPath() // 設(shè)置線寬和顏色 UIColor.yellow.setStroke() path.lineWidth = 1.0 let lineVWidth: CGFloat = height / 10.0 // 繪制水平線 for i in 0...10 { path.move(to: CGPoint(x: 0, y: CGFloat(i) * lineVWidth)) path.addLine(to: CGPoint(x: width, y: CGFloat(i) * lineVWidth)) } // 繪制垂直線 let lineHWidth: CGFloat = width / 10.0 for i in 0...10 { path.move(to: CGPoint(x: CGFloat(i) * lineHWidth, y: 0)) path.addLine(to: CGPoint(x: CGFloat(i) * lineHWidth, y: height)) } // 將路徑添加到視圖中并繪制 path.stroke() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } -
3.2、方格和曲線交叉點(diǎn)的坐標(biāo)獲取
//MARK: - CGMutablePath曲線-根據(jù)x坐標(biāo)獲取y坐標(biāo) extension BezierCurveView { //MARK: 根據(jù)某個(gè)點(diǎn)的x坐標(biāo)獲取y坐標(biāo) /// 根據(jù)某個(gè)點(diǎn)的x坐標(biāo)獲取y坐標(biāo) /// - Parameters: /// - x: x / y坐標(biāo) /// - path: CGMutablePath /// - Returns: description private func getPointXY(xy: CGFloat, path: CGPath, isX: Bool = true) -> CGPoint { var value: CGFloat = 0.0 var prevPoint = CGPoint.zero path.applyWithBlock { element in switch element.pointee.type { case .moveToPoint: prevPoint = element.pointee.points[0] case .addLineToPoint: let startPoint = prevPoint let endPoint = element.pointee.points[0] if isX { if xy >= startPoint.x && xy <= endPoint.x { let t = (xy - startPoint.x) / (endPoint.x - startPoint.x) value = startPoint.y + t * (endPoint.y - startPoint.y) } } else { if xy <= startPoint.y && xy >= endPoint.y { let t = (xy - startPoint.y) / (endPoint.y - startPoint.y) value = startPoint.x + t * (endPoint.x - startPoint.x) } } prevPoint = endPoint default: break } } return isX ? CGPoint(x: xy, y: value) : CGPoint(x: value, y: xy) } }
demo地址
更多的擴(kuò)展請(qǐng)參考另一個(gè)基礎(chǔ)庫JKSwiftExtension
