目录
- 曲线上点算法
- 点的手势处理:点击线生成点和距离线有效距离内拖动生成点
- 方格和曲线交叉点的坐标获取
一、曲线上点算法
1.1、思路:根据坐标上的点,计算控制点,从而再通过控制点之间分割成100个点(具体的控制点之间点的个数可以自己定义),通过每个控制点之间的计算生成点,最后串起来就是上图看到的曲线
-
1.2、部分代码
//MARK: 通过已知点绘制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) }
二、点的手势处理
-
2.1、点击线生成点
分析:点击线的话,首先是拿到点击点的坐标p0,根据这个坐标,获取这个点与线垂直和水平交叉点的坐标p1和p2,看这个两个点到p0距离
代码示例://MARK: 父视图点击手势 /// 父视图点击手势 /// - 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、点击点首先要在 左右两个点的矩形内,如果不在不生点 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 // 找到后面的点 nextPoint = item break } previousPoint = item } guard tapLocation.x > previousPoint.x && tapLocation.y < previousPoint.y && tapLocation.x < nextPoint.x && tapLocation.y > nextPoint.y else { debugPrint("❌不在矩形范围内", "previousPoint:\(previousPoint) nextPoint:\(nextPoint)") return } // 在矩形的范围内,确定添加的点事垂直点还是水平点 // 垂直点 let vPoint = getPointXY(xy: tapLocation.x, path: currentPath) // 水平点 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("✅在矩形范围内 ❌:不在有效距离:\(effectiveDistance) 内, 垂直距离:\(vLength) 水平距离:\(hLength)") return } // 在有效的范围内 var point: CGPoint = CGPoint() if vLength < hLength { point = vPoint debugPrint("✅在矩形范围内:取值垂直的点") } else { point = hPoint debugPrint("✅在矩形范围内:取值水平的点") } // 2、在矩形内,生成一个点 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) // 插入生成的点 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)) } // 加震动 let generator = UINotificationFeedbackGenerator() generator.notificationOccurred(.success) let param = getParamPointArray() dataClosure?(param.cmd_state, param.auxiliary_curve) }
-
2.2、距离线有效距离内拖动生成点
分析:拖动的话,首先是拿到点拖动起点的坐标p0,根据p0的坐标xy,分别获取曲线上的点p1和p2,同2.1一样拿到p0与两点的距离,看是否在有效距离effectiveDistance内,如果在看谁距离很近,近的则是点生成点的起点,从而拖动中点跟着移动,这个是利用的父视图的拖动手势
代码示例://MARK: 父视图拖动手势 /// 父视图拖动手势 /// - Parameter panGesture: 手势 @objc func superPanGester(panGesture: UIPanGestureRecognizer) { // 最多maxCircleViewNumber个点,包含两头的点 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 { // 在拖动开始的位置生成一个点 superPanInserTag = 100 + result.insertIndex // 2、在矩形内,生成一个点 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) // 插入生成的点 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() // 加震动 let generator = UINotificationFeedbackGenerator() generator.notificationOccurred(.success) } debugPrint("super-拖动开始: \(startPanLocation) superPanInserTag:\(superPanInserTag)") case .changed: let tapLocation = panGesture.location(in: self) if superPanInserTag > 0 { debugPrint("super-拖动中: \(tapLocation) inserTag:\(superPanInserTag)") // 添加的点跟着移动 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("❌不在矩形范围内", "previousPoint:\(previousPoint) nextPoint:\(nextPoint)") // 移除该点 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 } // 加震动 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-拖动结束 新的value") default: debugPrint("super-其他") } } //MARK: 是否响应父视图拖动的手势 /// 是否响应拖动的手势:实现 gestureRecognizer(_:shouldReceive:) 方法 func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { // 根据条件决定是否响应手势 if isCanUserInteractionEnabled { let location = touch.location(in: self) let result = isPointLine(point: location) return result.isEffectivePoint } else { return false } }
三、方格和曲线交叉点的坐标获取
-
3.1、方格
这个花方格就比较简单了,只需要两个for循环即可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 // 创建一个UIBezierPath对象 let path = UIBezierPath() // 设置线宽和颜色 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、方格和曲线交叉点的坐标获取
//MARK: - CGMutablePath曲线-根据x坐标获取y坐标 extension BezierCurveView { //MARK: 根据某个点的x坐标获取y坐标 /// 根据某个点的x坐标获取y坐标 /// - Parameters: /// - x: x / y坐标 /// - 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地址
更多的扩展请参考另一个基础库JKSwiftExtension