使用 UIBezierPath 绘制图形曲线
Posted
技术标签:
【中文标题】使用 UIBezierPath 绘制图形曲线【英文标题】:Draw Graph curves with UIBezierPath 【发布时间】:2012-12-05 08:38:59 【问题描述】:我正在我的应用程序中绘制图表。我的问题是我想将连接顶点的线绘制为曲线。目前我正在使用UIBezierPath
的函数addLineToPoint:
绘制它们。我想把它们画成曲线。我很清楚UIBezierPath
有以下两个函数来支持这个特性。
三次曲线:addCurveToPoint:controlPoint1:controlPoint2:
二次曲线:addQuadCurveToPoint:controlPoint:
但问题是我没有控制点。我只有两个端点。我都没有找到确定控制点的方法/公式。有人能帮我一下吗?如果有人可以提出一些替代方案,我将不胜感激......
【问题讨论】:
【参考方案1】:扩展 Abid Hussain 在 2012 年 12 月 5 日的回答。
我实现了代码,它可以工作,但结果看起来像这样:
稍加调整,我就能得到我想要的:
我所做的是将QuadCurveToPoint 也添加到中点,使用中点作为控制点。以下是基于 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;
【讨论】:
你的结果看起来不错。当时我使用github.com/amogh-talpallikar/smooth-curve-algorithm 绘制平滑的正弦曲线。为你的努力脱帽致敬。 如果你有两个连续下降的点会是什么样子? IE。你有 3, 1, 5, 10 而不是 1, 5, 3, 10?它会正确显示平滑增加吗? @Fogmeister 不会。我已经尝试过这种算法,虽然它非常适合交替上下笔画,但如果值像 6、10、12、18 一样,它会产生像直线一样的楼梯。非常感谢您的努力! 拖放解决方案!很好的答案 所以下面的 Roman 三次曲线确实解决了阶梯问题,并使曲线拟合得更好。我还对其进行了修补以处理变量 x 增量。【参考方案2】:使用@user1244109 的回答,我在 Swift 5 中实现了更好的算法。
class GraphView: UIView
var data: [CGFloat] = [2, 6, 12, 4, 5, 7, 5, 6, 6, 3]
didSet
setNeedsDisplay()
func coordYFor(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: coordYFor(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: coordYFor(index: 1)))
return path
var oldControlP: CGPoint?
for i in 1..<data.count
let p2 = CGPoint(x: step * CGFloat(i), y: coordYFor(index: i))
drawPoint(point: p2, color: UIColor.red, radius: 3)
var p3: CGPoint?
if i < data.count - 1
p3 = CGPoint(x: step * CGFloat(i + 1), y: coordYFor(index: i + 1))
let newControlP = controlPointForPoints(p1: p1, p2: p2, next: p3)
path.addCurve(to: p2, controlPoint1: oldControlP ?? p1, controlPoint2: newControlP ?? p2)
p1 = p2
oldControlP = antipodalFor(point: newControlP, center: p2)
return path;
/// located on the opposite side from the center point
func antipodalFor(point: CGPoint?, center: CGPoint?) -> CGPoint?
guard let p1 = point, 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)
/// halfway of two points
func midPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint
return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2);
/// Find controlPoint2 for addCurve
/// - Parameters:
/// - p1: first point of curve
/// - p2: second point of curve whose control point we are looking for
/// - next: predicted next point which will use antipodal control point for finded
func controlPointForPoints(p1: CGPoint, p2: CGPoint, next 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: antipodalFor(point: rightMidPoint, center: p2)!)
if p1.y.between(a: p2.y, b: controlPoint.y)
controlPoint.y = p1.y
else if p2.y.between(a: p1.y, b: controlPoint.y)
controlPoint.y = p2.y
let imaginContol = antipodalFor(point: controlPoint, center: p2)!
if p2.y.between(a: p3.y, b: imaginContol.y)
controlPoint.y = p2.y
if p3.y.between(a: p2.y, b: imaginContol.y)
let diffY = abs(p2.y - p3.y)
controlPoint.y = p2.y + diffY * (p3.y < p2.y ? 1 : -1)
// make lines easier
controlPoint.x += (p2.x - p1.x) * 0.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()
extension CGFloat
func between(a: CGFloat, b: CGFloat) -> Bool
return self >= Swift.min(a, b) && self <= Swift.max(a, b)
【讨论】:
非常好!我正在向后移植到 ObjC 并且有几个问题。从概念上讲,imaginFor 是做什么的?其次,为什么取abs然后有条件地乘以-1? (例如在 imaginFor 中,只需let newY = center.y*2 - p1.y
就像 newX)
@mackworth 是的,至少在我的测试中它看起来可以简化。
这个答案效果很好,与我测试过的其他答案不同,到目前为止没有问题。我什至已经将它转换为 SwiftUI,而且效果很好。
@DanielRyan 你愿意在某处发布你的 SwiftUI 版本吗?
@Gargoyle 我已经发布了我的答案。享受吧。【参考方案3】:
如果你只有两个点,那么你不能真正将它们外推成曲线。
如果你有两个以上的点(即沿着一条曲线的几个点),那么你可以粗略地画一条曲线来跟随它们。
如果您执行以下操作...
好的,假设你有 5 分。 p1、p2、p3、p4 和 p5。 您需要计算出每对点之间的中点。 m1 = p1 和 p2 的中点(以此类推)...
所以你有 m1、m2、m3 和 m4。
现在您可以将中点用作曲线各段的终点,并将这些点用作四边形曲线的控制点...
所以...
移动到点 m1。 将四边形曲线添加到点m2,以p2为控制点。
将四边形曲线添加到点m3,以p3为控制点。
将四边形曲线添加到点m4,以p4为控制点。
等等……
这会让你们远离曲线的末端(暂时不记得如何获得它们,抱歉)。
【讨论】:
但是兄弟,增减效果怎么样。例如,点的 y 坐标按顺序为 y1=10,y2=15,y=8。现在曲线 y1->y2 应该在线上方,曲线 y2->y3 应该在线下方。这意味着我们不能只将线的中点作为控制点,我们还需要将中点 Y 向上/向下平移。 你是对的,这只是对曲线的估计。点越靠近,曲线越准确。此外,曲线梯度变化越剧烈,离实际点越远。不过,这是获得相当准确估计的最快、最简单的方法。要获得更高的准确性将需要更困难的数学和更多的数学。在代码中推断曲线比用肉眼推断要困难得多:D 你好@Fogmeister,我正在做你上面建议的同样的事情,但我没有得到非常平滑的曲线。这是链接***.com/questions/20881721/…。有什么可以做得更好。我请求你帮帮我。等待您的回复【参考方案4】:所以我根据@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];
【讨论】:
【参考方案5】:我在这方面做了一些工作,并从 Catmull-Rom 样条曲线中获得了不错的结果。这是一个方法,它采用 CGPoints 数组(存储为 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
【讨论】:
【参考方案6】:一个好的解决方案是通过您的点创建一个直线UIBezierPath
,然后使用样条曲线来弯曲该线。查看另一个答案,它为 UIBezierPath 提供了一个执行 Catmull-Rom 样条的类别。 Drawing Smooth Curves - Methods Needed
【讨论】:
【参考方案7】:在分析来自@roman-filippov 的代码时,我对代码进行了一些简化。这是一个完整的 Swift Playground 版本,下面是一个 ObjC 版本。
我发现如果 x 增量不规则,那么当点在 x 轴上靠得很近时,原始算法会创建一些不幸的逆行线。简单地将控制点限制为不超过以下 x 值似乎可以解决问题,尽管我对此没有数学依据,只是实验。有两个部分标记为 //** added` 实现了此更改。
import UIKit
import PlaygroundSupport
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
PlaygroundPage.current.liveView = u
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 中的相同代码(或多或少。这不包括绘图例程本身,它确实允许点数组包含丢失的数据
+ (UIBezierPath *)pathWithPoints:(NSArray <NSValue *> *)points open:(BOOL) open
//based on Roman Filippov code: http://***.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;
【讨论】:
【参考方案8】:这是 @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 1..<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
【讨论】:
【参考方案9】:如果您需要每个点附近的“水平”曲线:
let path = createCurve(from: points, withSmoothness: 0.5) // 0.5 smoothness
let path = createCurve(from: points, withSmoothness: 0) // 0 smoothness
/// Create UIBezierPath
///
/// - Parameters:
/// - points: the points
/// - smoothness: the smoothness: 0 - no smooth at all, 1 - maximum smoothness
private func createCurve(from points: [CGPoint], withSmoothness smoothness: CGFloat, addZeros: Bool = false) -> UIBezierPath
let path = UIBezierPath()
guard points.count > 0 else return path
var prevPoint: CGPoint = points.first!
let interval = getXLineInterval()
if addZeros
path.move(to: CGPoint(x: interval.origin.x, y: interval.origin.y))
path.addLine(to: points[0])
else
path.move(to: points[0])
for i in 1..<points.count
let cp = controlPoints(p1: prevPoint, p2: points[i], smoothness: smoothness)
path.addCurve(to: points[i], controlPoint1: cp.0, controlPoint2: cp.1)
prevPoint = points[i]
if addZeros
path.addLine(to: CGPoint(x: prevPoint.x, y: interval.origin.y))
return path
/// Create control points with given smoothness
///
/// - Parameters:
/// - p1: the first point
/// - p2: the second point
/// - smoothness: the smoothness: 0 - no smooth at all, 1 - maximum smoothness
/// - Returns: two control points
private func controlPoints(p1: CGPoint, p2: CGPoint, smoothness: CGFloat) -> (CGPoint, CGPoint)
let cp1: CGPoint!
let cp2: CGPoint!
let percent = min(1, max(0, smoothness))
do
var cp = p2
// Apply smoothness
let x0 = max(p1.x, p2.x)
let x1 = min(p1.x, p2.x)
let x = x0 + (x1 - x0) * percent
cp.x = x
cp2 = cp
do
var cp = p1
// Apply smoothness
let x0 = min(p1.x, p2.x)
let x1 = max(p1.x, p2.x)
let x = x0 + (x1 - x0) * percent
cp.x = x
cp1 = cp
return (cp1, cp2)
/// Defines interval width, height (not used in this example) and coordinate of the first interval.
/// - Returns: (x0, y0, step, height)
internal func getXLineInterval() -> CGRect
return CGRect.zero
【讨论】:
getXLineInterval
方法有什么作用?
@Xernox 可以返回CGRect.zero
。解决方案更复杂,getXLineInterval
用于布置带有自定义填充的 Ox 标签(与曲线点略有不同,但接近它们)。【参考方案10】:
此答案是 Roman Filippov 答案的修改版本,以使其在 SwiftUI 上运行。 Roman's 是我测试过的最好的版本。
extension Path
/// This draws a curved path from a path of points given.
/// Modified from https://***.com/q/13719143/515455
mutating func quadCurvedPath(from data: [CGPoint])
var prevousPoint: CGPoint = data.first!
self.move(to: prevousPoint)
if (data.count == 2)
self.addLine(to: data[1])
return
var oldControlPoint: CGPoint?
for i in 1..<data.count
let currentPoint = data[i]
var nextPoint: CGPoint?
if i < data.count - 1
nextPoint = data[i + 1]
let newControlPoint = controlPointForPoints(p1: prevousPoint, p2: currentPoint, next: nextPoint)
self.addCurve(to: currentPoint, control1: oldControlPoint ?? prevousPoint, control2: newControlPoint ?? currentPoint)
prevousPoint = currentPoint
oldControlPoint = antipodalFor(point: newControlPoint, center: currentPoint)
/// Located on the opposite side from the center point
func antipodalFor(point: CGPoint?, center: CGPoint?) -> CGPoint?
guard let p1 = point, let center = center else
return nil
let newX = 2.0 * center.x - p1.x
let newY = 2.0 * center.y - p1.y
return CGPoint(x: newX, y: newY)
/// Find the mid point of two points
func midPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint
return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2);
/// Find control point
/// - Parameters:
/// - p1: first point of curve
/// - p2: second point of curve whose control point we are looking for
/// - next: predicted next point which will use antipodal control point for finded
func controlPointForPoints(p1: CGPoint, p2: CGPoint, next 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: antipodalFor(point: rightMidPoint, center: p2)!)
if p1.y.between(a: p2.y, b: controlPoint.y)
controlPoint.y = p1.y
else if p2.y.between(a: p1.y, b: controlPoint.y)
controlPoint.y = p2.y
let imaginContol = antipodalFor(point: controlPoint, center: p2)!
if p2.y.between(a: p3.y, b: imaginContol.y)
controlPoint.y = p2.y
if p3.y.between(a: p2.y, b: imaginContol.y)
let diffY = abs(p2.y - p3.y)
controlPoint.y = p2.y + diffY * (p3.y < p2.y ? 1 : -1)
// make lines easier
controlPoint.x += (p2.x - p1.x) * 0.1
return controlPoint
extension CGFloat
func between(a: CGFloat, b: CGFloat) -> Bool
return self >= Swift.min(a, b) && self <= Swift.max(a, b)
然后您可以像这样在 SwiftUI 代码中使用它:
ZStack(alignment: .topLeading)
// Draw the line.
Path path in
path.quadCurvedPath(from: linePoints)
.stroke(graphData.colour, style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
【讨论】:
以上是关于使用 UIBezierPath 绘制图形曲线的主要内容,如果未能解决你的问题,请参考以下文章