iOS Swift实现Label文本随滑块上下弯曲(无需添加子层)
问题描述
我有一个Label,希望通过滑块控制其文本向上或向下弯曲。目前我已通过以下代码实现该功能:
func drawCurvedString(on layer: CALayer, text: NSAttributedString, angle: CGFloat, radius: CGFloat) { var radAngle = angle.radians let textSize = text.boundingRect( with: CGSize(width: .max, height: .max), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil) .integral .size let perimeter: CGFloat = 2 * .pi * radius let textAngle: CGFloat = textSize.width / perimeter * 2 * .pi var textRotation: CGFloat = 0 var textDirection: CGFloat = 0 if angle > CGFloat(10).radians, angle < CGFloat(170).radians { // bottom string textRotation = 0.5 * .pi textDirection = -2 * .pi radAngle += textAngle / 2 } else { // top string textRotation = 1.5 * .pi textDirection = 2 * .pi radAngle -= textAngle / 2 } for c in 0..<text.length { let letter = text.attributedSubstring(from: NSRange(c..<c+1)) let charSize = letter.boundingRect( with: CGSize(width: .max, height: .max), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil) .integral .size let letterAngle = (charSize.width / perimeter) * textDirection let x = radius * cos(radAngle + (letterAngle / 2)) let y = radius * sin(radAngle + (letterAngle / 2)) let singleChar = drawText( on: layer, text: letter, frame: CGRect( x: (layer.frame.size.width / 2) - (charSize.width / 2) + x, y: (layer.frame.size.height / 2) - (charSize.height / 2) + y, width: charSize.width, height: charSize.height)) layer.addSublayer(singleChar) singleChar.transform = CATransform3DMakeAffineTransform(CGAffineTransform(rotationAngle: radAngle - textRotation)) radAngle += letterAngle } } func drawText(on layer: CALayer, text: NSAttributedString, frame: CGRect) -> CATextLayer { let textLayer = CATextLayer() textLayer.frame = frame textLayer.string = text textLayer.alignmentMode = kCAAlignmentCenter textLayer.contentsScale = UIScreen.main.scale return textLayer }
但问题在于这段代码会添加文本子层,我想要在Swift中实现无需添加子层的方案,恳请帮助。
解决方案
可以通过自定义UIView,在draw(_ rect:)方法中直接使用Core Graphics绘制弯曲文本,不需要添加任何子层。以下是实现代码:
import UIKit class CurvedLabel: UIView { var attributedText: NSAttributedString = NSAttributedString(string: "") { didSet { setNeedsDisplay() } } var curveRadius: CGFloat = 100 { didSet { setNeedsDisplay() } } // 控制弯曲方向:true为向上弯(顶部弧形),false为向下弯(底部弧形) var curveUpwards: Bool = false { didSet { setNeedsDisplay() } } override func draw(_ rect: CGRect) { super.draw(rect) guard let context = UIGraphicsGetCurrentContext(), attributedText.length > 0 else { return } let textSize = attributedText.boundingRect( with: CGSize(width: .max, height: .max), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil ).integral.size let perimeter = 2 * .pi * curveRadius let totalTextAngle = textSize.width / perimeter * 2 * .pi var currentAngle: CGFloat var angleDirection: CGFloat let rotationOffset: CGFloat if curveUpwards { // 顶部弧形:文本沿上半圆排列,方向顺时针 rotationOffset = 1.5 * .pi angleDirection = 2 * .pi currentAngle = (.pi - totalTextAngle / 2) } else { // 底部弧形:文本沿下半圆排列,方向逆时针 rotationOffset = 0.5 * .pi angleDirection = -2 * .pi currentAngle = totalTextAngle / 2 } let center = CGPoint(x: bounds.midX, y: bounds.midY) for charIndex in 0..<attributedText.length { let charRange = NSRange(location: charIndex, length: 1) let charAttr = attributedText.attributedSubstring(from: charRange) let charSize = charAttr.boundingRect( with: CGSize(width: .max, height: .max), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil ).integral.size let charAngle = (charSize.width / perimeter) * angleDirection // 计算当前字符的圆心角位置 let charCenterAngle = currentAngle + charAngle / 2 // 计算字符绘制的位置(基于圆心偏移) let charPosition = CGPoint( x: center.x + curveRadius * cos(charCenterAngle), y: center.y + curveRadius * sin(charCenterAngle) ) // 保存当前上下文状态 context.saveGState() // 移动上下文到字符位置,旋转对应角度 context.translateBy(x: charPosition.x, y: charPosition.y) context.rotate(by: charCenterAngle - rotationOffset) // 绘制字符,锚点设为中心 charAttr.draw(at: CGPoint(x: -charSize.width/2, y: -charSize.height/2)) // 恢复上下文状态 context.restoreGState() currentAngle += charAngle } } }
使用方式
- 在代码中创建或在Storyboard中添加
CurvedLabel实例 - 设置
attributedText、curveRadius和curveUpwards属性,配合滑块控制参数:
// 示例:滑块控制弯曲半径 radiusSlider.addTarget(self, action: #selector(radiusChanged(_:)), for: .valueChanged) // 示例:切换弯曲方向 directionSwitch.addTarget(self, action: #selector(directionChanged(_:)), for: .valueChanged) @objc func radiusChanged(_ slider: UISlider) { curvedLabel.curveRadius = CGFloat(slider.value) } @objc func directionChanged(_ `switch`: UISwitch) { curvedLabel.curveUpwards = `switch`.isOn }
说明
- 该方案直接在视图的绘制上下文里渲染每个字符,不会创建任何子层
- 通过
setNeedsDisplay()在参数变化时触发重绘,保证实时更新 - 保留了原代码的弯曲逻辑,同时优化了角度计算和上下文操作
内容的提问来源于stack exchange,提问作者Asim Iftikhar Abbasi
相关产品推荐
相关产品推荐

