Apr*_*rog 1 core-graphics android-custom-view swift
我正在创建一个绘图应用程序,在其中使用自定义可交互视图,我正在绘制一条开放路径,我希望它的角是圆角的。在android中我可以添加cornerPathEffect但我无法在iOS中执行此操作
路径的圆角

许多基于路径的绘图库都有一个专门设计的路径运算符,可以使绘制圆角变得更容易。UIBezierPath没有,但addArc在较低级别CGMutablePath和SwiftUIPath上都有一个版本。但是,如果您查看 Mozilla 的 Web 画布方法CanvasRenderingContext2D.arcTo()的文档,就会更容易理解该运算符的作用,其中包含带有图片的很好的示例。
然而,仍然存在一些问题:
\naddArc方法来圆化你的角落。这似乎是一个需要解决的有趣问题,因此我编写了一个小 Swift 包,可以圆角 a CGPath、 aUIBezierPath或 SwiftUI Path。这是一个演示:
该包可在我的github 上的 RoundedPath 存储库中找到。我还将在这里粘贴代码以供后代使用:
\nimport CoreGraphics\n\n/// One segment of a `CGPath`. A segment is an actual drawing command, not just a move command.\nfileprivate enum Segment {\n /// A straight line segment with the given start and end points.\n case line(start: CGPoint, end: CGPoint)\n\n /// A quadratic B\xc3\xa9zier segment ending at the last given point.\n case quad(CGPoint, end: CGPoint)\n\n /// A cubic B\xc3\xa9zier segment ending at the last given point.\n case cubic(CGPoint, CGPoint, end: CGPoint)\n\n var end: CGPoint {\n switch self {\n case .line(start: _, end: let end), .quad(_, let end), .cubic(_, _, let end):\n return end\n }\n }\n}\n\n/// A complete subpath of a `CGPath`, with at least one segment and no moves or closePaths in the middle.\nfileprivate struct Subpath {\n /// The point at which `segments[0]` starts. Note that a `Segment` doesn\'t store its own start point.\n var firstPoint: CGPoint\n\n /// All my segments. Non-empty. If I\'m closed, this ends with `.line(firstPoint)`.\n var segments: [Segment] = []\n\n /// True the subpath ended with .closeSubpath.\n var isClosed: Bool = false\n}\n\nextension CGPath {\n /// Create a copy of myself in which sharp corners are rounded.\n ///\n /// - parameter radius: A radius to apply to sharp corners, which are corners between two straight-line segments. If a corner is an endpoint of a curve, it is not rounded.\n /// - returns: A path in which each meeting of two straight-line segments has been rounded with a radius of `radius`, if possible. A smaller radius is used in corners where the line segments are too short to use the full `radius`.\n public func copy(roundingCornersToRadius radius: CGFloat) -> CGPath {\n guard radius > 0 else { return self }\n\n let copy = CGMutablePath()\n\n var currentSubpath: Subpath? = nil\n var currentPoint = CGPoint.zero\n\n func append(_ segment: Segment) {\n switch currentSubpath {\n case nil:\n currentSubpath = .init(firstPoint: .zero, segments: [segment])\n case .some(var subpath):\n currentSubpath = nil\n subpath.segments.append(segment)\n currentSubpath = .some(subpath)\n }\n }\n\n self.applyWithBlock {\n let points = $0.pointee.points\n\n switch $0.pointee.type {\n case .moveToPoint:\n if let currentSubpath, !currentSubpath.segments.isEmpty {\n copy.append(currentSubpath, withCornerRadius: radius)\n }\n currentSubpath = .init(firstPoint: points[0])\n currentPoint = points[0]\n\n case .addLineToPoint:\n append(.line(start: currentPoint, end: points[0]))\n currentPoint = points[0]\n\n case .addQuadCurveToPoint:\n append(.quad(points[0], end: points[1]))\n currentPoint = points[1]\n\n case .addCurveToPoint:\n append(.cubic(points[0], points[1], end: points[2]))\n currentPoint = points[2]\n\n case .closeSubpath:\n if var currentSubpath {\n currentSubpath.segments.append(.line(start: currentPoint, end: currentSubpath.firstPoint))\n currentSubpath.isClosed = true\n copy.append(currentSubpath, withCornerRadius: radius)\n currentPoint = currentSubpath.firstPoint\n }\n currentSubpath = nil\n\n @unknown default:\n break\n }\n }\n\n if let currentSubpath, !currentSubpath.segments.isEmpty {\n copy.append(currentSubpath, withCornerRadius: radius)\n }\n\n return copy\n }\n}\n\nextension CGMutablePath {\n fileprivate func append(_ subpath: Subpath, withCornerRadius radius: CGFloat) {\n var priorSegment: Optional<Segment>\n\n // The overall strategy:\n //\n // - I don\'t draw the straight part of a line segment while processing the line segment itself.\n // - When I process a line segment, if the prior segment was also a line, I draw the straight part of the prior segment and the rounded corner where the segments meet.\n // - When I process a non-line segment, if the prior segment was a line, I draw the straight part of the prior segment.\n //\n // At each rounded corner, I clamp the radius such that the curve consumes no more than half of each segment.\n\n if\n subpath.isClosed,\n case .line(start: let lastStart, end: let lastEnd) = subpath.segments.last!,\n case .line(start: _, end: _) = subpath.segments.first!\n {\n // The subpath is closed, and the first and last segments are both lines. That means I need to round the corner between them when I process the first segment. I need to initialize currentPoint and priorSegment such that my normal line segment handling will draw the correct corner.\n\n if subpath.segments.count < 3 {\n // There are only one or two segments in this closed subpath. Since it\'s closed, there are two possibilities:\n //\n // - there is only one segment of zero length, or\n // - the second (and last) segment is just the first segment with the endpoints reversed.\n //\n // Either way, the path cannot have a visible corner to be rounded, so I don\'t need to do any special initialization.\n\n move(to: subpath.firstPoint)\n priorSegment = nil\n } else {\n // There is indeed a roundable corner between the last and first segments. Since I\'ll clamp the radius to consume no more than half of each of those segments, the midpoint of the last segment is a safe value for currentPoint.\n move(to: .midpoint(lastStart, lastEnd))\n priorSegment = subpath.segments.last!\n }\n } else {\n // It\'s not a closed subpath, or the first or last segment isn\'t a line, so there\'s no roundable corner at the start.\n move(to: subpath.firstPoint)\n priorSegment = nil\n }\n\n /// Call this when starting to process a non-line segment, to draw the straight part of priorSegment if needed.\n func finishPriorLineIfNeeded() {\n if\n case .some(.line(start: _, end: let end)) = priorSegment,\n end != currentPoint\n {\n addLine(to: end)\n }\n }\n\n for currentSegment in subpath.segments {\n // Invariants:\n // - If priorSegment is nil, currentPoint is subpath.firstPoint.\n // - If priorSegment is a line, currentPoint is somewhere on that segment, and priorSegment is only drawn up to currentPoint.\n // - If priorSegment is non-nil and not a line, currentPoint is the end point of priorSegment and prior is fully drawn.\n\n switch currentSegment {\n case .line(start: let t1, end: let t2):\n if case .some(.line(start: let t0, end: _)) = priorSegment {\n // At least part of priorSegment is undrawn. This addArc draws any undrawn part of priorSegment, and draws the circular arc at the corner, but doesn\'t draw any of currentSegment after the arc. It leaves currentPoint at the endpoint of the arc, which is somewhere on currentSegment.\n\n let cr = clampedRadius(\n forCorner: t1,\n priorTangent: .midpoint(t0, t1),\n nextTangent: .midpoint(t1, t2),\n proposedRadius: radius\n )\n addArc(tangent1End: t1, tangent2End: t2, radius: cr)\n }\n\n // I don\'t draw the rest of currentSegment here because some part of it may need to be replaced by an arc.\n\n case .quad(let c, end: let end):\n finishPriorLineIfNeeded()\n addQuadCurve(to: end, control: c)\n\n case .cubic(let c1, let c2, end: let end):\n finishPriorLineIfNeeded()\n addCurve(to: end, control1: c1, control2: c2)\n }\n\n priorSegment = currentSegment\n }\n\n if subpath.isClosed {\n closeSubpath()\n }\n\n else if case .some(.line(start: _, end: let end)) = priorSegment, end != currentPoint {\n addLine(to: end)\n }\n }\n}\n\nextension CGPoint {\n fileprivate static func midpoint(_ p0: Self, _ p1: Self) -> Self {\n return .init(x: 0.5 * p0.x + 0.5 * p1.x, y: 0.5 * p0.y + 0.5 * p1.y)\n }\n}\n\nfunc clampedRadius(forCorner corner: CGPoint, priorTangent: CGPoint, nextTangent: CGPoint, proposedRadius: CGFloat) -> CGFloat {\n guard\n let transform = CGAffineTransform.standardizing(origin: corner, unit: priorTangent)\n else { return 0 }\n\n /// `transform` is a conformal transform that transforms `corner` to the origin and `priorTangent` to (1, 0), which is the construction required by `clamp(r:under:)`.\n\n let scale = hypot(transform.a, transform.c)\n let p = nextTangent.applying(transform)\n let rScaled = proposedRadius * scale\n let rScaledClamped = clamp(r: rScaled, under: p)\n return rScaledClamped / scale\n}\n\nextension CGAffineTransform {\n /// - parameter origin: A point to be transformed to `.zero`.\n /// - parameter unit: A point to be transformed to `(1, 0)`.\n /// - returns: The unique conformal transform that transforms `origin` to `.zero` and transforms `unit` to `(1, 0)`, if it exists.\n fileprivate static func standardizing(origin: CGPoint, unit: CGPoint) -> Self? {\n let v = CGPoint(x: unit.x - origin.x, y: unit.y - origin.y)\n let q = v.x * v.x + v.y * v.y\n guard q != 0 else { return nil }\n let a = v.x / q\n let c = v.y / q\n return Self(\n a, -c,\n c, a,\n -(a * origin.x + c * origin.y), c * origin.x - a * origin.y\n )\n }\n}\n\n\n/// Consider this construction:\n///\n/// p\n/// \xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x96\x9e\n/// \xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x96\x9e\n/// \xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x96\x9e\n/// \xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x96\x9e\n/// \xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x96\x9e\n/// \xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x96\x9e\n/// \xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x96\x9e\n/// \xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x96\x9e\xe2\x96\x9a\n/// \xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x96\x9e\xe2\x80\x83\xe2\x80\x83\xe2\x96\x9a\n/// \xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x96\x9e\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x96\x9a c = (d, r)\n/// \xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x96\x9e\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x96\x8c\n/// \xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x96\x9e\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x96\x8c\n/// \xe2\x80\x83\xe2\x80\x83\xe2\x96\x9e\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x96\x8c\n/// \xe2\x80\x83\xe2\x96\x9e\xe2\x80\x83 \xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x80\x83\xe2\x96\x8c\n/// \xe2\x96\x9f\xe2\x96\x84\xe2\x96\x84\xe2\x96\x84\xe2\x96\x84\xe2\x96\x84\xe2\x96\x84\xe2\x96\x84\xe2\x96\x84\xe2\x96\x84\xe2\x96\x84\xe2\x96\x99\xe2\x96\x84\xe2\x96\x84\xe2\x96\x84\n/// 0 d (1,0)\n///\n/// `c` is distance `r` from the x axis and also distance `r` from the line to `p`. The coordinates of `c` are `(d, r)`.\n///\n/// I am given `p` and `r`, with `p.y \xe2\x89\xa0 0`. Note that `p` could be less than 1 unit from the origin; it is not required to be far away as in the diagram.\n///\n/// My job is to compute `d`. If `abs(d)` is in the range 0 ... min(1, length(p), I return `r`. Otherwise, I compute the closest value to `r`\n/// that would put `abs(d)` in the required range, and return that closest value.\nfileprivate func clamp(r: CGFloat, under p: CGPoint) -> CGFloat {\n // Since `r` is given,\n\n let pLength = hypot(Double(p.x), Double(p.y))\n\n /// Let theta be the angle between the x axis and vector `p`.\n ///\n /// Therefore `c`, being equidistant from the x axis and the line through `p`, is at angle theta/2 from the x axis.\n ///\n /// So `d` = `r` / tan theta/2.\n ///\n /// Trig identity: tan theta/2 = (1 - cos theta) / sin theta.\n ///\n /// cos theta = p.x / pLength\n /// sin theta = p.y / pLength\n /// tan theta/2 = (1 - cos theta) / sin theta\n /// = (pLength - p.x) / p.y\n ///\n /// d = r * p.y / (pLength - p.x)\n ///\n /// Note that if `pLength == p.x`, `d` is undefined. But that only happens if `p.y == 0`, which violates my precondition.\n\n let d = r * p.y / (pLength - p.x)\n\n let dLimit = min(1, pLength)\n if abs(d) <= dLimit { return r }\n\n return abs(dLimit * (pLength - p.x) / p.y)\n}\nRun Code Online (Sandbox Code Playgroud)\n
| 归档时间: |
|
| 查看次数: |
828 次 |
| 最近记录: |