如何在 SwiftUI 中将文本以自定义弧形居中?

问题描述 投票:0回答:1

我想将

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)
    }
}
swiftui geometry shapes text-alignment
1个回答
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 }

输出:

© www.soinside.com 2019 - 2024. All rights reserved.