You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在SwiftUI中实现文本随角度从直线平滑过渡到圆形

SwiftUI圆形文本视图优化:平滑过渡与完美圆形实现

问题描述

我尝试在SwiftUI中创建圆形文本视图,需求如下:

  • 文本初始呈直线排列
  • 通过滑块调整圆弧角度,角度从0°到360°时,文本逐步从直线过渡为圆弧,最终形成完美正圆

当前代码可实现圆形文本排列,但存在两个问题:

  1. 过渡不够平滑,尤其是角度较小时文本仍保持直线状态
  2. 角度达到360°时无法形成完美闭合的圆形

当前代码

import SwiftUI

struct CirculerTextView: View {
    @State private var arcDegrees: Double = 0
    let text: String = "Happy Mornings"
    
    var body: some View {
        VStack(spacing: 40) {
            GeometryReader { geometry in
                CirculerText(text: text, arcDegrees: arcDegrees, size: geometry.size)
                    .frame(height: 200)
            }
            .frame(height: 200)
            .border(Color.gray.opacity(0.3))
            
            VStack {
                Slider(value: $arcDegrees, in: 0...360, step: 1)
                    .padding(.horizontal)
                Text("Arc Degrees: \(Int(arcDegrees))")
                    .font(.subheadline)
            }
        }
        .padding()
    }
}

struct CirculerText: View {
    let text: String
    let arcDegrees: Double
    let size: CGSize
    
    var body: some View {
        let letters = Array(text)
        let letterCount = letters.count
        let chordLength = size.width * 0.5
        let spacing = letterCount > 1 ? chordLength / CGFloat(letterCount - 1) : 0
        let startX = (size.width - chordLength) / 2
        let centerY = size.height / 2
        let centerX = size.width / 2
        
        let totalArcRad = arcDegrees * .pi / 360
        let safeArcRad = max(abs(totalArcRad), 0.0001)
        let radius = chordLength / (2 * CGFloat(sin(safeArcRad / 2)))
        let circleCenter = CGPoint(x: centerX, y: centerY)
        
        return ZStack {
            ForEach(0..<letterCount, id: \.self) { index in
                let letterAngle = -totalArcRad / 2 + totalArcRad * Double(index) / Double(letterCount - 1)
                let x = circleCenter.x + radius * CGFloat(sin(letterAngle))
                let y = circleCenter.y - radius * CGFloat(cos(letterAngle))
                
                let finalPos = CGPoint(x: x, y: y)
                let textRotation = arcDegrees == 0 ? 0 : -angleInDegrees(letterAngle)
                
                Text(String(letters[index]))
                    .font(.title)
                    .position(finalPos)
                    .rotationEffect(.degrees(textRotation))
            }
        }
    }
    
    func angleInDegrees(_ radians: Double) -> Double {
        return radians * 360 / .pi
    }
}

struct CirculerTextView_Previews: PreviewProvider {
    static var previews: some View {
        CirculerTextView()
    }
}

效果对比

预期效果

预期效果:文本平滑过渡为闭合圆形

当前效果

当前效果:小角度文本仍呈直线,360°无法完美闭合

修改方案与优化代码

关键优化点

  1. 平滑过渡处理:当角度较小时,通过插值混合直线布局与圆弧布局的位置,避免直接切换导致的生硬感;同时限制最小半径,防止小角度下半径过大导致字母位置几乎不变。
  2. 360°完美圆形处理:当角度接近360°时,切换到圆周均匀分布逻辑,固定半径为容器高度的一半,确保首尾字母无缝衔接。

修改后的完整代码

import SwiftUI

struct CirculerTextView: View {
    @State private var arcDegrees: Double = 0
    let text: String = "Happy Mornings"
    
    var body: some View {
        VStack(spacing: 40) {
            GeometryReader { geometry in
                CirculerText(text: text, arcDegrees: arcDegrees, size: geometry.size)
                    .frame(height: 200)
            }
            .frame(height: 200)
            .border(Color.gray.opacity(0.3))
            
            VStack {
                Slider(value: $arcDegrees, in: 0...360, step: 1)
                    .padding(.horizontal)
                Text("Arc Degrees: \(Int(arcDegrees))")
                    .font(.subheadline)
            }
        }
        .padding()
    }
}

struct CirculerText: View {
    let text: String
    let arcDegrees: Double
    let size: CGSize
    
    var body: some View {
        let letters = Array(text)
        let letterCount = letters.count
        guard letterCount > 0 else { return EmptyView() }
        
        let center = CGPoint(x: size.width / 2, y: size.height / 2)
        let maxRadius = min(size.width, size.height) / 2.5 // 限制最大半径,避免超出容器
        let totalArcRad = arcDegrees * .pi / 360
        
        // 区分360°完美圆形和过渡阶段
        let isFullCircle = abs(arcDegrees - 360) < 1e-2
        
        return ZStack {
            ForEach(0..<letterCount, id: \.self) { index in
                let letterIndex = Double(index)
                let totalLetters = Double(letterCount)
                
                var targetPos: CGPoint
                var rotationAngle: Double
                
                if isFullCircle {
                    // 360°时均匀分布在圆周上
                    let angle = 2 * .pi * letterIndex / totalLetters
                    targetPos = CGPoint(
                        x: center.x + maxRadius * CGFloat(cos(angle)),
                        y: center.y + maxRadius * CGFloat(sin(angle))
                    )
                    rotationAngle = angleInDegrees(angle) + 90 // 让文字垂直于圆周切线
                } else {
                    // 过渡阶段:混合直线与圆弧布局
                    let linearX = center.x - (size.width * 0.4) + (size.width * 0.8) * letterIndex / (totalLetters - 1)
                    let linearPos = CGPoint(x: linearX, y: center.y)
                    
                    // 计算圆弧布局的位置,限制半径避免过大
                    let safeArcRad = max(abs(totalArcRad), 0.01)
                    let chordLength = size.width * 0.8
                    var radius = chordLength / (2 * CGFloat(sin(safeArcRad / 2)))
                    radius = min(radius, maxRadius * 5) // 限制半径上限,避免小角度下位置不变
                    
                    let letterAngle = -totalArcRad / 2 + totalArcRad * letterIndex / (totalLetters - 1)
                    let arcPos = CGPoint(
                        x: center.x + radius * CGFloat(sin(letterAngle)),
                        y: center.y - radius * CGFloat(cos(letterAngle))
                    )
                    
                    // 插值混合直线与圆弧位置,实现平滑过渡
                    let transitionProgress = min(arcDegrees / 60, 1) // 前60°完成从直线到圆弧的过渡
                    targetPos = CGPoint(
                        x: linearX * (1 - transitionProgress) + arcPos.x * transitionProgress,
                        y: linearPos.y * (1 - transitionProgress) + arcPos.y * transitionProgress
                    )
                    
                    rotationAngle = transitionProgress * (-angleInDegrees(letterAngle))
                }
                
                Text(String(letters[index]))
                    .font(.title)
                    .position(targetPos)
                    .rotationEffect(.degrees(rotationAngle))
            }
        }
    }
    
    func angleInDegrees(_ radians: Double) -> Double {
        return radians * 180 / .pi
    }
}

struct CirculerTextView_Previews: PreviewProvider {
    static var previews: some View {
        CirculerTextView()
    }
}

优化说明

  • 平滑过渡:通过transitionProgress变量,在角度从0到60°时逐步将字母从直线位置过渡到圆弧位置,避免小角度下的生硬感。
  • 完美圆形:当角度接近360°时,切换到圆周均匀分布逻辑,固定半径并调整文字旋转角度,确保文字垂直于圆周切线,首尾字母无缝衔接。
  • 半径限制:添加了最大半径限制,避免小角度下因弦长公式导致半径过大,字母位置几乎不变的问题。

内容的提问来源于stack exchange,提问作者HeWhoRemains

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 14:30:05