如何在弯曲文本视图中的字母之间创建相等的间距?

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

我正在绘制一个以弧形为中心的弯曲文本视图,我希望每个字母之间的间距相等。

我尝试将文本字体更新为

.font(.system(size: 14, design: .monospaced))
,这使字母之间的间距相等,但它不是正确的字体。

目前,间距如下所示:

理想情况下,它应该是这样的(但使用正确的字体):

struct ContentView: View {

    @State private var letterWidths: [Int: Double] = [:]
    private let size: CGFloat = 300

    var body: some View {
        ZStack {
            ArcShape(start: 180, end: 230)
                .frame(width: size, height: size)
                .foregroundColor(.gray)
            textView("AVENIR", start: 180, end: 230)

            ArcShape(start: 233, end: 300)
                .frame(width: size, height: size)
                .foregroundColor(.gray)
            textView("DEMIBOLD", start: 233, end: 300)
        }
        .frame(width: size)
    }

    func textView(_ title: String, start: CGFloat, end: CGFloat) -> some View {
        ZStack {
            let firstLetterAngle = calculateLetterAngle(at: 0)
            let lastLetterAngle = calculateLetterAngle(at: title.count - 1)
            let titleAngleDifference: Angle = lastLetterAngle - firstLetterAngle

            let segmentAngleDifference: Angle = .degrees(end) - .degrees(start)

            let titleOffsetAngle = (segmentAngleDifference - titleAngleDifference) / 2

            ForEach(Array(title.enumerated()), id: \.offset) { index, letter in
                VStack {
                    Text(String(letter))
                        .font(.custom("AvenirNext-DemiBold", size: 14))
                        .kerning(3)
                        .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(calculateLetterAngle(at: index))
            }
            // to center title in arc shape
            .frame(width: size, height: size * 0.75)
            .rotationEffect(titleOffsetAngle + .degrees(start) + .degrees(90))
        }
    }

    private func calculateLetterAngle(at letterPosition: Int) -> Angle {
        let times2Pi: (CGFloat) -> CGFloat = { $0 * 2 * .pi }

        let radius: CGFloat = 125
        let circumference = times2Pi(radius)

        let letterAngle = times2Pi(
            letterWidths
                .filter { $0.key < letterPosition }
                .map(\.value)
                .reduce(0, +) / circumference
        )

        return .degrees(letterAngle * (180 / .pi)) // to convert radians to degrees
    }

}

struct LetterWidthPreferenceKey: PreferenceKey {
    static var defaultValue: Double = 0
    static func reduce(value: inout Double, nextValue: () -> Double) {
        value = nextValue()
    }
}

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 textview
1个回答
0
投票

您问这个问题是因为不清楚如何使用我为您的其他问题提供的解决方案

像这样:

var textRadius: CGFloat {
    (size / 2) - 42
}

var body: some View {
    ZStack {
        ArcShape(start: 180, end: 230)
            .frame(width: size, height: size)
            .foregroundColor(.gray)
        CurvedText(
            string: "AVENIR",
            radius: textRadius
        )
        .offset(y: -textRadius)
        .rotationEffect(.degrees(((180 + 230) / 2) + 90))

        ArcShape(start: 233, end: 300)
            .frame(width: size, height: size)
            .foregroundColor(.gray)
        CurvedText(
            string: "DEMIBOLD",
            radius: textRadius
        )
        .offset(y: -textRadius)
        .rotationEffect(.degrees(((233 + 300) / 2) + 90))
    }
    .foregroundColor(.white)
    .frame(width: size)
}

Screenshot

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