我想将
textView
置于 ArcShape
的中心,目前 textView
的 startAngle 设置为 ArcShape
的 startAngle。到目前为止,我已经尝试找到 ArcShape
的 startAngle 和 endAngle 的中点,但这会将 textView
移动到 ArcShape
的右侧。
如何计算
startAngle
的 textView
,使其以 ArcShape
为中心?
这就是目前的样子:
理想情况下,我希望它看起来像这样:
import SwiftUI
struct ContentView: View {
@State private var letterWidths: [Int: Double] = [:]
let size: CGFloat = 300
var body: some View {
ZStack {
ArcShape(startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 230))
.foregroundColor(.green)
textView("HELLO", startAngle: Angle(degrees: 180))
ArcShape(startAngle: Angle(degrees: 233), endAngle: Angle(degrees: 300))
.foregroundColor(.red)
textView("BYEBYE", startAngle: Angle(degrees: 233))
}
.frame(width: size, height: size)
.frame(height: size / 2)
.offset(y: size / 4.5)
}
@ViewBuilder
func textView(_ title: String, startAngle: Angle) -> some View {
ZStack {
ForEach(Array(title.enumerated()), id: \.offset) { index, letter in
VStack {
Text(String(letter))
.font(.system(size: 14, weight: .semibold, design: .monospaced))
.kerning(2)
.background(
GeometryReader { geometry in // using this to get the width of EACH letter
Color.clear
.preference(
key: LetterWidthPreferenceKey.self,
value: geometry.size.width
)
}
)
.onPreferenceChange(LetterWidthPreferenceKey.self, perform: { width in
letterWidths[index] = width
})
Spacer()
}
.rotationEffect(fetchAngle(at: index))
}
.frame(width: size, height: size * 0.75)
.rotationEffect(startAngle + .degrees(90))
}
}
func fetchAngle(at letterPosition: Int) -> Angle {
let timesPi: (Double) -> Double = { $0 * .pi }
let radius: Double = 125
let circumference = timesPi(radius)
let finalAngle = timesPi(
letterWidths
.filter { $0.key <= letterPosition }
.map(\.value)
.reduce(0, +) / circumference
)
return .radians(finalAngle)
}
}
struct LetterWidthPreferenceKey: PreferenceKey {
static var defaultValue: Double = 0
static func reduce(value: inout Double, nextValue: () -> Double) {
value = nextValue()
}
}
#Preview {
ContentView()
}
private struct ArcShape: Shape {
let startAngle: Angle?
let endAngle: Angle?
func path(in rect: CGRect) -> Path {
let shorterLength = min(rect.width, rect.height)
let path = UIBezierPath(
roundedArcCenter: rect.center,
innerRadius: (shorterLength / 2) * 0.54,
outerRadius: (shorterLength / 2) * 0.90,
startAngle: startAngle ?? .zero,
endAngle: endAngle ?? .zero,
cornerRadiusPercentage: 0.01
)
return Path(path.cgPath)
}
}
extension UIBezierPath {
public convenience init(roundedArcCenter center: CGPoint, innerRadius: CGFloat, outerRadius: CGFloat, startAngle: Angle, endAngle: Angle, cornerRadiusPercentage: CGFloat) {
let maxCornerRadiusBasedOnInnerArcLength = abs((endAngle - startAngle).radians) * innerRadius / 2
let maxCornerRadiusBasedOnOuterArcLength = abs((endAngle - startAngle).radians) * outerRadius / 2
let maxCornerRadiusBasedOnEndCapLength = (outerRadius - innerRadius) / 2
let outerCornerRadius = min(2 * .pi * outerRadius * cornerRadiusPercentage, maxCornerRadiusBasedOnOuterArcLength, maxCornerRadiusBasedOnEndCapLength)
let outerCornerRadiusPercentage = outerCornerRadius / (2 * .pi * outerRadius)
let innerCornerRadius = min(2 * .pi * innerRadius * outerCornerRadiusPercentage, maxCornerRadiusBasedOnInnerArcLength, maxCornerRadiusBasedOnEndCapLength)
let innerInsetAngle = Angle(radians: innerCornerRadius / innerRadius)
let outerInsetAngle = Angle(radians: outerCornerRadius / outerRadius)
self.init()
var arcStartAngle = (startAngle + outerInsetAngle).radians
var arcEndAngle = (endAngle - outerInsetAngle).radians
addArc(
withCenter: .zero,
radius: outerRadius,
startAngle: min(arcStartAngle, arcEndAngle),
endAngle: max(arcStartAngle, arcEndAngle),
clockwise: true
)
addCorner(
to: .pointOnCircle(radius: outerRadius - outerCornerRadius, angle: endAngle),
controlPoint: .pointOnCircle(radius: outerRadius, angle: endAngle)
)
addLine(to: .pointOnCircle(radius: innerRadius + innerCornerRadius, angle: endAngle))
addCorner(
to: .pointOnCircle(radius: innerRadius, angle: endAngle - innerInsetAngle),
controlPoint: .pointOnCircle(radius: innerRadius, angle: endAngle)
)
arcStartAngle = (endAngle - innerInsetAngle).radians
arcEndAngle = (startAngle + innerInsetAngle).radians
addArc(
withCenter: .zero,
radius: innerRadius,
startAngle: max(arcStartAngle, arcEndAngle),
endAngle: min(arcStartAngle, arcEndAngle),
clockwise: false
)
addCorner(
to: .pointOnCircle(radius: innerRadius + innerCornerRadius, angle: startAngle),
controlPoint: .pointOnCircle(radius: innerRadius, angle: startAngle)
)
addLine(to: .pointOnCircle(radius: outerRadius - outerCornerRadius, angle: startAngle))
addCorner(
to: .pointOnCircle(radius: outerRadius, angle: startAngle + outerInsetAngle),
controlPoint: .pointOnCircle(radius: outerRadius, angle: startAngle)
)
apply(.init(translationX: center.x, y: center.y))
}
private func addCorner(to: CGPoint, controlPoint: CGPoint) {
let circleApproximationConstant = 0.551915
addCurve(
to: to,
controlPoint1: currentPoint + (controlPoint - currentPoint) * circleApproximationConstant,
controlPoint2: to + (controlPoint - to) * circleApproximationConstant
)
}
}
private extension CGPoint {
static func pointOnCircle(radius: CGFloat, angle: Angle) -> CGPoint {
CGPoint(x: radius * Darwin.cos(angle.radians), y: radius * Darwin.sin(angle.radians))
}
static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
static func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}
static func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
}
}
public extension CGRect {
var center: CGPoint {
CGPoint(x: size.width / 2.0, y: size.height / 2.0)
}
}
使用当前代码,您可以采用与水平/垂直居中类似的方式来完成此操作。
您计算可用空间以及文本占用的空间。从前者中减去后者并除以 2。然后将文本“移动”该量。当然,在这种情况下,我们所说的“空间”是以度/弧度来衡量的。
这意味着
textView
也应该包含结束角度:
// pass the same end angles as you pass to the ArcShapes
func textView(_ title: String, startAngle: Angle, endAngle: Angle) -> some View
// before the ForEach
let firstAngle = fetchAngle(at: 0)
let lastAngle = fetchAngle(at: title.count - 1)
let textAngleDifference = lastAngle - firstAngle
let availableSpace = endAngle - startAngle
let offset = (availableSpace - textAngleDifference) / 2
应用于
rotationEffect
的 ForEach
应添加 offset
:
.rotationEffect(offset + startAngle + .degrees(90))
最后,请注意您的
fetchAngle
不正确。 “Hello”中的“H”应该完全水平(没有offset
),但您可以看到屏幕截图中的情况并非如此。 filter
中的比较应该是<
,而不是<=
。
.filter { $0.key < letterPosition }
输出: