SwiftUI中如何在自定义ArcShape内居中显示文本?
问题描述
我希望将textView在ArcShape内居中显示,目前textView的startAngle设置为ArcShape的startAngle。我尝试过计算ArcShape的startAngle与endAngle的中点,但这会导致textView偏移到ArcShape右侧。请问该如何计算textView的startAngle,使其在ArcShape中居中?
当前效果

理想效果

解决方案
核心逻辑是:先计算文本整体占用的弧度,再将文本向左偏移该弧度的一半,结合Arc的中点角度进行定位,让文本中点与Arc中点对齐,实现居中效果。
修改步骤
- 新增方法计算文本总占用弧度,确定偏移量
- 调整textView的起始角度计算逻辑,融合Arc中点角度与文本偏移量
完整修改代码
import SwiftUI struct ContentView: View { @State private var letterWidths: [Int: Double] = [:] let size: CGFloat = 300 let textRadius: Double = 125 // 提取为常量,方便统一修改 var body: some View { ZStack { // 绿色Arc和对应的文本 ArcShape(startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 230)) .foregroundColor(.green) textView( "HELLO", arcStartAngle: Angle(degrees: 180), arcEndAngle: Angle(degrees: 230) ) // 红色Arc和对应的文本 ArcShape(startAngle: Angle(degrees: 233), endAngle: Angle(degrees: 300)) .foregroundColor(.red) textView( "BYEBYE", arcStartAngle: Angle(degrees: 233), arcEndAngle: Angle(degrees: 300) ) } .frame(width: size, height: size) .frame(height: size / 2) .offset(y: size / 4.5) } @ViewBuilder func textView(_ title: String, arcStartAngle: Angle, arcEndAngle: Angle) -> some View { // 计算Arc的中点角度 let arcMidAngle = Angle( degrees: (arcStartAngle.degrees + arcEndAngle.degrees) / 2 ) // 计算文本总占用的弧度,再取一半作为偏移量 let textHalfRadian = totalTextRadian(for: title) / 2 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 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) // 核心调整:用Arc中点角度减去文本半弧度,再加上90度的基础旋转 .rotationEffect(arcMidAngle - .radians(textHalfRadian) + .degrees(90)) } } // 计算单个字母的旋转角度 func fetchAngle(at letterPosition: Int) -> Angle { let circumference = .pi * textRadius let totalWidth = letterWidths .filter { $0.key <= letterPosition } .map(\.value) .reduce(0, +) let angleRadian = .pi * (totalWidth / circumference) return .radians(angleRadian) } // 计算文本整体占用的总弧度 func totalTextRadian(for title: String) -> Double { let circumference = .pi * textRadius let totalWidth = letterWidths .filter { $0.key < title.count } .map(\.value) .reduce(0, +) return .pi * (totalWidth / circumference) } } 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) } }
关键修改说明
- 参数调整:给textView方法新增
arcEndAngle参数,用于计算Arc的中点角度 - 新增文本弧度计算:
totalTextRadian方法统计所有字母总宽度对应的弧度,取一半作为文本的偏移量 - 旋转逻辑优化:不再直接使用Arc的startAngle,而是通过
Arc中点角度 - 文本半弧度 + 90度基础旋转,让文本中点与Arc中点对齐,实现精准居中
内容的提问来源于stack exchange,提问作者Nadia Siddiqah
相关产品推荐
相关产品推荐

