用UIBezierPath绘制graphics曲线

我在我的应用程序中绘制图表。 我的问题是,我想绘制线连接顶点作为曲线。 目前我正在使用UIBezierPath的函数addLineToPoint:绘制它们。 我想绘制他们作为曲线。 我很清楚, UIBezierPath有两个function来支持这个function。

三次曲线: addCurveToPoint:controlPoint1:controlPoint2:addCurveToPoint:controlPoint1:controlPoint2:addCurveToPoint:controlPoint1:controlPoint2:二次曲线: addQuadCurveToPoint:controlPoint:

但问题是我没有控制点。 我所拥有的是两个终点。 我也没有find确定控制点的方法/公式。 有人能帮我一下吗? 我会感激,如果有人可以build议一些替代…

展开在12月5日的阿比德·侯赛因的答案。

我实现了代码,它的工作,但结果如下所示: 在这里输入图像说明

几乎没有调整,我能够得到我想要的: 在这里输入图像说明

我所做的是将中间点作为控制点,将中途点加到中点上。 以下是基于Abid样本的代码; 希望它可以帮助别人。

 + (UIBezierPath *)quadCurvedPathWithPoints:(NSArray *)points { UIBezierPath *path = [UIBezierPath bezierPath]; NSValue *value = points[0]; CGPoint p1 = [value CGPointValue]; [path moveToPoint:p1]; if (points.count == 2) { value = points[1]; CGPoint p2 = [value CGPointValue]; [path addLineToPoint:p2]; return path; } for (NSUInteger i = 1; i < points.count; i++) { value = points[i]; CGPoint p2 = [value CGPointValue]; CGPoint midPoint = midPointForPoints(p1, p2); [path addQuadCurveToPoint:midPoint controlPoint:controlPointForPoints(midPoint, p1)]; [path addQuadCurveToPoint:p2 controlPoint:controlPointForPoints(midPoint, p2)]; p1 = p2; } return path; } static CGPoint midPointForPoints(CGPoint p1, CGPoint p2) { return CGPointMake((p1.x + p2.x) / 2, (p1.y + p2.y) / 2); } static CGPoint controlPointForPoints(CGPoint p1, CGPoint p2) { CGPoint controlPoint = midPointForPoints(p1, p2); CGFloat diffY = abs(p2.y - controlPoint.y); if (p1.y < p2.y) controlPoint.y += diffY; else if (p1.y > p2.y) controlPoint.y -= diffY; return controlPoint; } 

如果你只有两个点,那么你不能把它们外推到曲线上。

如果你有两个以上的点(即曲线上的几个点),那么你可以大致绘制一条曲线来跟随它们。

如果你做以下…

好吧,说你有5分。 p1,p2,p3,p4和p5。 你需要计算出每对点之间的中点。 m1 = p1和p2的中点(依此类推)…

所以你有m1,m2,m3和m4。

现在,您可以使用中点作为曲线的端点,并将点作为四边形曲线的控制点。

所以…

移动到点m1。 以p2作为控制点,将四边形曲线添加到点m2。

以p3作为控制点,将四边形曲线添加到点m3。

添加四边形曲线,以p4作为控制点指向m4。

等等…

这将使你们远离曲线的末端(现在不记得如何得到它们,对不起)。

使用@ user1244109的答案我在Swift 3中实现了一个更好的algorithm。 图表与我的实施

 var data: [CGFloat] = [0, 0, 0, 0, 0, 0] { didSet { setNeedsDisplay() } } func coordXFor(index: Int) -> CGFloat { return bounds.height - bounds.height * data[index] / (data.max() ?? 0) } override func draw(_ rect: CGRect) { let path = quadCurvedPath() UIColor.black.setStroke() path.lineWidth = 1 path.stroke() } func quadCurvedPath() -> UIBezierPath { let path = UIBezierPath() let step = bounds.width / CGFloat(data.count - 1) var p1 = CGPoint(x: 0, y: coordXFor(index: 0)) path.move(to: p1) drawPoint(point: p1, color: UIColor.red, radius: 3) if (data.count == 2) { path.addLine(to: CGPoint(x: step, y: coordXFor(index: 1))) return path } var oldControlP: CGPoint? for i in 1..<data.count { let p2 = CGPoint(x: step * CGFloat(i), y: coordXFor(index: i)) drawPoint(point: p2, color: UIColor.red, radius: 3) var p3: CGPoint? if i == data.count - 1 { p3 = nil } else { p3 = CGPoint(x: step * CGFloat(i + 1), y: coordXFor(index: i + 1)) } let newControlP = controlPointForPoints(p1: p1, p2: p2, p3: p3) path.addCurve(to: p2, controlPoint1: oldControlP ?? p1, controlPoint2: newControlP ?? p2) p1 = p2 oldControlP = imaginFor(point1: newControlP, center: p2) } return path; } func imaginFor(point1: CGPoint?, center: CGPoint?) -> CGPoint? { guard let p1 = point1, let center = center else { return nil } let newX = 2 * center.x - p1.x let diffY = abs(p1.y - center.y) let newY = center.y + diffY * (p1.y < center.y ? 1 : -1) return CGPoint(x: newX, y: newY) } func midPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint { return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2); } func controlPointForPoints(p1: CGPoint, p2: CGPoint, p3: CGPoint?) -> CGPoint? { guard let p3 = p3 else { return nil } let leftMidPoint = midPointForPoints(p1: p1, p2: p2) let rightMidPoint = midPointForPoints(p1: p2, p2: p3) var controlPoint = midPointForPoints(p1: leftMidPoint, p2: imaginFor(point1: rightMidPoint, center: p2)!) // this part needs for optimization if p1.y < p2.y { if controlPoint.y < p1.y { controlPoint.y = p1.y } if controlPoint.y > p2.y { controlPoint.y = p2.y } } else { if controlPoint.y > p1.y { controlPoint.y = p1.y } if controlPoint.y < p2.y { controlPoint.y = p2.y } } let imaginContol = imaginFor(point1: controlPoint, center: p2)! if p2.y < p3.y { if imaginContol.y < p2.y { controlPoint.y = p2.y } if imaginContol.y > p3.y { let diffY = abs(p2.y - p3.y) controlPoint.y = p2.y + diffY * (p3.y < p2.y ? 1 : -1) } } else { if imaginContol.y > p2.y { controlPoint.y = p2.y } if imaginContol.y < p3.y { let diffY = abs(p2.y - p3.y) controlPoint.y = p2.y + diffY * (p3.y < p2.y ? 1 : -1) } } return controlPoint } func drawPoint(point: CGPoint, color: UIColor, radius: CGFloat) { let ovalPath = UIBezierPath(ovalIn: CGRect(x: point.x - radius, y: point.y - radius, width: radius * 2, height: radius * 2)) color.setFill() ovalPath.fill() } 

所以我find了一个基于@ Fogmeister的答案的工作。

  UIBezierPath *path = [UIBezierPath bezierPath]; [path setLineWidth:3.0]; [path setLineCapStyle:kCGLineCapRound]; [path setLineJoinStyle:kCGLineJoinRound]; // actualPoints are my points array stored as NSValue NSValue *value = [actualPoints objectAtIndex:0]; CGPoint p1 = [value CGPointValue]; [path moveToPoint:p1]; for (int k=1; k<[actualPoints count];k++) { NSValue *value = [actualPoints objectAtIndex:k]; CGPoint p2 = [value CGPointValue]; CGPoint centerPoint = CGPointMake((p1.x+p2.x)/2, (p1.y+p2.y)/2); // See if your curve is decreasing or increasing // You can optimize it further by finding point on normal of line passing through midpoint if (p1.y<p2.y) { centerPoint = CGPointMake(centerPoint.x, centerPoint.y+(abs(p2.y-centerPoint.y))); }else if(p1.y>p2.y){ centerPoint = CGPointMake(centerPoint.x, centerPoint.y-(abs(p2.y-centerPoint.y))); } [path addQuadCurveToPoint:p2 controlPoint:centerPoint]; p1 = p2; } [path stroke]; 

我在这方面做了一些工作,并从Catmull-Rom样条中获得了相当不错的结果。 这是一个方法,它需要一个CGPoint(存储为NSValues)数组,并将行添加到给定的视图。

 - (void)addBezierPathBetweenPoints:(NSArray *)points toView:(UIView *)view withColor:(UIColor *)color andStrokeWidth:(NSUInteger)strokeWidth { UIBezierPath *path = [UIBezierPath bezierPath]; float granularity = 100; [path moveToPoint:[[points firstObject] CGPointValue]]; for (int index = 1; index < points.count - 2 ; index++) { CGPoint point0 = [[points objectAtIndex:index - 1] CGPointValue]; CGPoint point1 = [[points objectAtIndex:index] CGPointValue]; CGPoint point2 = [[points objectAtIndex:index + 1] CGPointValue]; CGPoint point3 = [[points objectAtIndex:index + 2] CGPointValue]; for (int i = 1; i < granularity ; i++) { float t = (float) i * (1.0f / (float) granularity); float tt = t * t; float ttt = tt * t; CGPoint pi; pi.x = 0.5 * (2*point1.x+(point2.x-point0.x)*t + (2*point0.x-5*point1.x+4*point2.x-point3.x)*tt + (3*point1.x-point0.x-3*point2.x+point3.x)*ttt); pi.y = 0.5 * (2*point1.y+(point2.y-point0.y)*t + (2*point0.y-5*point1.y+4*point2.y-point3.y)*tt + (3*point1.y-point0.y-3*point2.y+point3.y)*ttt); if (pi.y > view.frame.size.height) { pi.y = view.frame.size.height; } else if (pi.y < 0){ pi.y = 0; } if (pi.x > point0.x) { [path addLineToPoint:pi]; } } [path addLineToPoint:point2]; } [path addLineToPoint:[[points objectAtIndex:[points count] - 1] CGPointValue]]; CAShapeLayer *shapeView = [[CAShapeLayer alloc] init]; shapeView.path = [path CGPath]; shapeView.strokeColor = color.CGColor; shapeView.fillColor = [UIColor clearColor].CGColor; shapeView.lineWidth = strokeWidth; [shapeView setLineCap:kCALineCapRound]; [view.layer addSublayer:shapeView]; } 

我在这里使用它https://github.com/johnyorke/JYGraphViewController

一个好的解决scheme是通过你的点创build一个直UIBezierPath ,然后使用样条来曲线。 看看这个其他答案给出了执行Catmull-Rom样条曲线的UIBezierpath的类别。 绘制平滑的曲线 – 需要的方法

这里的@ user1244109的答案转换为Swift 3

 private func quadCurvedPath(with points:[CGPoint]) -> UIBezierPath { let path = UIBezierPath() var p1 = points[0] path.move(to: p1) if points.count == 2 { path.addLine(to: points[1]) return path } for i in 0..<points.count { let mid = midPoint(for: (p1, points[i])) path.addQuadCurve(to: mid, controlPoint: controlPoint(for: (mid, p1))) path.addQuadCurve(to: points[i], controlPoint: controlPoint(for: (mid, points[i]))) p1 = points[i] } return path } private func midPoint(for points: (CGPoint, CGPoint)) -> CGPoint { return CGPoint(x: (points.0.x + points.1.x) / 2 , y: (points.0.y + points.1.y) / 2) } private func controlPoint(for points: (CGPoint, CGPoint)) -> CGPoint { var controlPoint = midPoint(for: points) let diffY = abs(points.1.y - controlPoint.y) if points.0.y < points.1.y { controlPoint.y += diffY } else if points.0.y > points.1.y { controlPoint.y -= diffY } return controlPoint } 

在分析@ roman-filippov的代码时,我简化了一些代码。 这是一个完整的Swift游乐场版本,还有一个ObjC版本。

我发现如果x增量不规则,那么当x轴上的点靠近时,原始algorithm会产生一些不幸的逆行。 简单地把控制点约束为不超过下面的x值似乎解决了这个问题,尽pipe我没有math的理由,只是试验。 有两个部分标记为// **添加`执行此更改。

 import UIKit infix operator ° func °(x: CGFloat, y: CGFloat) -> CGPoint { return CGPoint(x: x, y: y) } extension UIBezierPath { func drawPoint(point: CGPoint, color: UIColor, radius: CGFloat) { let ovalPath = UIBezierPath(ovalIn: CGRect(x: point.x - radius, y: point.y - radius, width: radius * 2, height: radius * 2)) color.setFill() ovalPath.fill() } func drawWithLine (point: CGPoint, color: UIColor) { let startP = self.currentPoint self.addLine(to: point) drawPoint(point: point, color: color, radius: 3) self.move(to: startP) } } class TestView : UIView { var step: CGFloat = 1.0; var yMaximum: CGFloat = 1.0 var xMaximum: CGFloat = 1.0 var data: [CGPoint] = [] { didSet { xMaximum = data.reduce(-CGFloat.greatestFiniteMagnitude, { max($0, $1.x) }) yMaximum = data.reduce(-CGFloat.greatestFiniteMagnitude, { max($0, $1.y) }) setNeedsDisplay() } } func scale(point: CGPoint) -> CGPoint { return CGPoint(x: bounds.width * point.x / xMaximum , y: (bounds.height - bounds.height * point.y / yMaximum)) } override func draw(_ rect: CGRect) { if data.count <= 1 { return } let path = cubicCurvedPath() UIColor.black.setStroke() path.lineWidth = 1 path.stroke() } func cubicCurvedPath() -> UIBezierPath { let path = UIBezierPath() var p1 = scale(point: data[0]) path.drawPoint(point: p1, color: UIColor.red, radius: 3) path.move(to: p1) var oldControlP = p1 for i in 0..<data.count { let p2 = scale(point:data[i]) path.drawPoint(point: p2, color: UIColor.red, radius: 3) var p3: CGPoint? = nil if i < data.count - 1 { p3 = scale(point:data [i+1]) } let newControlP = controlPointForPoints(p1: p1, p2: p2, p3: p3) //uncomment the following four lines to graph control points //if let controlP = newControlP { // path.drawWithLine(point:controlP, color: UIColor.blue) //} //path.drawWithLine(point:oldControlP, color: UIColor.gray) path.addCurve(to: p2, controlPoint1: oldControlP , controlPoint2: newControlP ?? p2) oldControlP = imaginFor(point1: newControlP, center: p2) ?? p1 //***added to algorithm if let p3 = p3 { if oldControlP.x > p3.x { oldControlP.x = p3.x } } //*** p1 = p2 } return path; } func imaginFor(point1: CGPoint?, center: CGPoint?) -> CGPoint? { //returns "mirror image" of point: the point that is symmetrical through center. //aka opposite of midpoint; returns the point whose midpoint with point1 is center) guard let p1 = point1, let center = center else { return nil } let newX = center.x + center.x - p1.x let newY = center.y + center.y - p1.y return CGPoint(x: newX, y: newY) } func midPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint { return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2); } func clamp(num: CGFloat, bounds1: CGFloat, bounds2: CGFloat) -> CGFloat { //ensure num is between bounds. if (bounds1 < bounds2) { return min(max(bounds1,num),bounds2); } else { return min(max(bounds2,num),bounds1); } } func controlPointForPoints(p1: CGPoint, p2: CGPoint, p3: CGPoint?) -> CGPoint? { guard let p3 = p3 else { return nil } let leftMidPoint = midPointForPoints(p1: p1, p2: p2) let rightMidPoint = midPointForPoints(p1: p2, p2: p3) let imaginPoint = imaginFor(point1: rightMidPoint, center: p2) var controlPoint = midPointForPoints(p1: leftMidPoint, p2: imaginPoint!) controlPoint.y = clamp(num: controlPoint.y, bounds1: p1.y, bounds2: p2.y) let flippedP3 = p2.y + (p2.y-p3.y) controlPoint.y = clamp(num: controlPoint.y, bounds1: p2.y, bounds2: flippedP3); //***added: controlPoint.x = clamp (num:controlPoint.x, bounds1: p1.x, bounds2: p2.x) //*** // print ("p1: \(p1), p2: \(p2), p3: \(p3), LM:\(leftMidPoint), RM:\(rightMidPoint), IP:\(imaginPoint), fP3:\(flippedP3), CP:\(controlPoint)") return controlPoint } } let u = TestView(frame: CGRect(x: 0, y: 0, width: 700, height: 600)); u.backgroundColor = UIColor.white u.data = [0.5 ° 1, 1 ° 3, 2 ° 5, 4 ° 9, 8 ° 15, 9.4 ° 8, 9.5 ° 10, 12 ° 4, 13 ° 10, 15 ° 3, 16 ° 1] 

和ObjC中相同的代码(或多或less,这不包括绘图例程本身,它允许点数组包含缺失的数据

 + (UIBezierPath *)pathWithPoints:(NSArray <NSValue *> *)points open:(BOOL) open { //based on Roman Filippov code: http://stackoverflow.com/a/40203583/580850 //open means allow gaps in path. UIBezierPath *path = [UIBezierPath bezierPath]; CGPoint p1 = [points[0] CGPointValue]; [path moveToPoint:p1]; CGPoint oldControlPoint = p1; for (NSUInteger pointIndex = 1; pointIndex< points.count; pointIndex++) { CGPoint p2 = [points[pointIndex] CGPointValue]; //note: mark missing data with CGFloatMax if (p1.y >= CGFloatMax || p2.y >= CGFloatMax) { if (open) { [path moveToPoint:p2]; } else { [path addLineToPoint:p2]; } oldControlPoint = p2; } else { CGPoint p3 = CGPointZero; if (pointIndex +1 < points.count) p3 = [points[pointIndex+1] CGPointValue] ; if (p3.y >= CGFloatMax) p3 = CGPointZero; CGPoint newControlPoint = controlPointForPoints2(p1, p2, p3); if (!CGPointEqualToPoint( newControlPoint, CGPointZero)) { [path addCurveToPoint: p2 controlPoint1:oldControlPoint controlPoint2: newControlPoint]; oldControlPoint = imaginForPoints( newControlPoint, p2); //**added to algorithm if (! CGPointEqualToPoint(p3,CGPointZero)) { if (oldControlPoint.x > p3.x ) { oldControlPoint.x = p3.x; } //*** } else { [path addCurveToPoint: p2 controlPoint1:oldControlPoint controlPoint2: p2]; oldControlPoint = p2; } } p1 = p2; } return path; } static CGPoint imaginForPoints(CGPoint point, CGPoint center) { //returns "mirror image" of point: the point that is symmetrical through center. if (CGPointEqualToPoint(point, CGPointZero) || CGPointEqualToPoint(center, CGPointZero)) { return CGPointZero; } CGFloat newX = center.x + (center.x-point.x); CGFloat newY = center.y + (center.y-point.y); if (isinf(newY)) { newY = BEMNullGraphValue; } return CGPointMake(newX,newY); } static CGFloat clamp(CGFloat num, CGFloat bounds1, CGFloat bounds2) { //ensure num is between bounds. if (bounds1 < bounds2) { return MIN(MAX(bounds1,num),bounds2); } else { return MIN(MAX(bounds2,num),bounds1); } } static CGPoint controlPointForPoints2(CGPoint p1, CGPoint p2, CGPoint p3) { if (CGPointEqualToPoint(p3, CGPointZero)) return CGPointZero; CGPoint leftMidPoint = midPointForPoints(p1, p2); CGPoint rightMidPoint = midPointForPoints(p2, p3); CGPoint imaginPoint = imaginForPoints(rightMidPoint, p2); CGPoint controlPoint = midPointForPoints(leftMidPoint, imaginPoint); controlPoint.y = clamp(controlPoint.y, p1.y, p2.y); CGFloat flippedP3 = p2.y + (p2.y-p3.y); controlPoint.y = clamp(controlPoint.y, p2.y, flippedP3); //**added to algorithm controlPoint.x = clamp(controlPoint.x, p1.x, p2.x); //** return controlPoint; }