CATextLayer 位置和转换问题

CATextLayer position and Transformation issue

我正在尝试构建一个控件,例如附加的圆形图像,其中每个部分的多个片段 space 相等。段数可以根据提供的数组更改。

到目前为止,我已经使用 CAShapeLayer 和 UIBezierPath 开发了这个。还在每个形状层的中心添加了文本。 到目前为止,我已经添加了用于生成此控件的代码。

let circleLayer = CALayer()
func createCircle(_ titleArray:[String]) {
     update(bounds: bounds, titleArray: titleArray)
     containerView.layer.addSublayer(circleRenderer.circleLayer)
}


 func update(bounds: CGRect, titleArray: [String]) {
        let position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
        circleLayer.position = position
        circleLayer.bounds = bounds
        update(titleArray: titles)
    }

func update(titleArray: [String]) {
   let center = CGPoint(x: circleLayer.bounds.size.width / 2.0, y: circleLayer.bounds.size.height / 2.0)
   let radius:CGFloat = min(circleLayer.bounds.size.width, circleLayer.bounds.size.height) / 2
   let segmentSize = CGFloat((Double.pi*2) / Double(titleArray.count))
   
   for i in 0..<titleArray.count {
       let startAngle = segmentSize*CGFloat(i) - segmentSize/2
       let endAngle = segmentSize*CGFloat(i+1) - segmentSize/2
       let midAngle = (startAngle+endAngle)/2
       
       let shapeLayer = CAShapeLayer()
       shapeLayer.fillColor = UIColor.random.cgColor
       
       let bezierPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
       bezierPath.addLine(to: center)
       shapeLayer.path = bezierPath.cgPath
       
       let height = titleArray[i].height(withConstrainedWidth: radius-20, font: UIFont.systemFont(ofSize: 15))
       let frame = shapeLayer.path?.boundingBoxOfPath ?? CGRect.zero
       let textLayer = TextLayer()
       textLayer.frame = CGRect(x: 0, y: 0, width: 70, height: height)
       textLayer.position = CGPoint(x: frame.center.x, y: frame.center.y)
       textLayer.fontSize = 15
       textLayer.contentsScale = UIScreen.main.scale
       textLayer.alignmentMode = .center
       textLayer.string = titleArray[i]
       textLayer.isWrapped = true
       textLayer.backgroundColor = UIColor.black.cgColor
       textLayer.foregroundColor = UIColor.white.cgColor
       textLayer.transform = CATransform3DMakeRotation(midAngle, 0.0, 0.0, 1.0)
       shapeLayer.addSublayer(textLayer)
       circleLayer.addSublayer(shapeLayer)
   }

}

circleLayer 添加为 superlayer,包含整个 UIView 区域。 我的要求是在带角度的形状内添加垂直和水平居中的文本。我遇到了在形状内居中文本而角度没问题的问题。

谢谢

编辑: 如果我删除 textLayer 旋转代码,那么它看起来像这个图像。

你的标签不是 "centered" 因为你使用的是楔形的几何中心 bounding-box:

您需要做的是计算一个 "inner" 圆,其半径为全半径的 1/2,然后找到该圆上的点来放置您的标签。

所以,首先我们计算圆:

然后平分每个角,求圆上的点:

然后计算标签的bounding-box(我用的是radius * 0.6的max-width),把那个框的中心放在圆上的点上,然后旋转文字图层:

结果,没有 "guides":

注意:对于这些图像,我使用 radius * 0.55 - 或者比半径的 1/2 稍微远一些 - 作为 "inner circle"。这给了我稍微好一点的外观,因为当我们到达圆心时楔子变窄了。将其更改为 radius * 0.6 可能看起来更好。

下面是生成这个视图的代码:

struct Wedge {
    var color: UIColor = .cyan
    var label: String = ""
}

class WedgeView: UIView {

    var wedges: [Wedge] = []

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {

    }

    override func layoutSubviews() {
        super.layoutSubviews()

        layer.sublayers?.forEach { [=10=].removeFromSuperlayer() }

        setup()

    }

    func setup() -> Void {

        // initialize local variables
        var startAngle: CGFloat = 0

        var outerRadius: CGFloat = 0.0
        var halfRadius: CGFloat = 0.0

        // initialize local constants
        let viewCenter: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
        let diameter = bounds.width

        let fontHeight: CGFloat = ceil(12.0 * (bounds.height / 300.0))
        let textLayerFont = UIFont.systemFont(ofSize: fontHeight, weight: .light)

        outerRadius = diameter * 0.5
        halfRadius = outerRadius * 0.55

        let labelMaxWidth:CGFloat = outerRadius * 0.6

        startAngle = -(.pi * (1.0 / CGFloat(wedges.count)))

        for i in 0..<wedges.count {
            let endAngle = startAngle + 2 * .pi * (1.0 / CGFloat(wedges.count))
            let shape = CAShapeLayer()
            let path: UIBezierPath = UIBezierPath()
            path.addArc(withCenter: viewCenter, radius: outerRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
            path.addLine(to: viewCenter)
            path.close()
            shape.path = path.cgPath

            shape.fillColor = wedges[i].color.cgColor
            shape.strokeColor = UIColor.black.cgColor

            self.layer.addSublayer(shape)

            let textLayer = CATextLayer()

            textLayer.font = textLayerFont
            textLayer.fontSize = fontHeight
            let string = wedges[i].label
            textLayer.string = string

            textLayer.foregroundColor = UIColor.white.cgColor
            textLayer.backgroundColor = UIColor.black.cgColor

            textLayer.isWrapped = true
            textLayer.alignmentMode = CATextLayerAlignmentMode.center
            textLayer.contentsScale = UIScreen.main.scale

            let bisectAngle = startAngle + ((endAngle - startAngle) * 0.5)
            let p = CGPoint.pointOnCircle(center: viewCenter, radius: halfRadius, angle: bisectAngle)

            var textLayerframe = CGRect(x: 0, y: 0, width: labelMaxWidth, height: 0)
            let h = string.getLableHeight(labelMaxWidth, usingFont: textLayerFont)
            textLayerframe.size.height = h

            textLayerframe.origin.x = p.x - (textLayerframe.size.width * 0.5)
            textLayerframe.origin.y = p.y - (textLayerframe.size.height * 0.5)

            textLayer.frame = textLayerframe

            self.layer.addSublayer(textLayer)

            textLayer.transform = CATransform3DMakeRotation(bisectAngle, 0.0, 0.0, 1.0)

            // uncomment this block to show the dashed-lines
            /*
            let biLayer = CAShapeLayer()
            let dash = UIBezierPath()
            dash.move(to: viewCenter)
            dash.addLine(to: p)
            biLayer.strokeColor = UIColor.yellow.cgColor
            biLayer.lineDashPattern = [4, 4]
            biLayer.path = dash.cgPath
            self.layer.addSublayer(biLayer)
            */

            startAngle = endAngle
        }

        // uncomment this block to show the half-radius circle
        /*
        let tempLayer: CAShapeLayer = CAShapeLayer()
        tempLayer.path = UIBezierPath(ovalIn: bounds.insetBy(dx: outerRadius - halfRadius, dy: outerRadius - halfRadius)).cgPath
        tempLayer.fillColor = UIColor.clear.cgColor
        tempLayer.strokeColor = UIColor.green.cgColor
        tempLayer.lineWidth = 1.0
        self.layer.addSublayer(tempLayer)
        */

    }

}

class WedgesWithRotatedLabelsViewController: UIViewController {

    let wedgeView: WedgeView = WedgeView()

    var wedges: [Wedge] = []

    let colors: [UIColor] = [
        UIColor(red: 1.00, green: 0.00, blue: 0.00, alpha: 1.0),
        UIColor(red: 0.00, green: 0.50, blue: 0.00, alpha: 1.0),
        UIColor(red: 0.00, green: 0.00, blue: 1.00, alpha: 1.0),
        UIColor(red: 1.00, green: 0.50, blue: 0.50, alpha: 1.0),
        UIColor(red: 0.00, green: 0.75, blue: 0.00, alpha: 1.0),
        UIColor(red: 0.50, green: 0.50, blue: 1.00, alpha: 1.0),
    ]
    let labels: [String] = [
        "This is long text for Label 1",
        "Label 2",
        "Longer Label 3",
        "Label 4",
        "Label 5",
        "Label 6",
    ]

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)

        for (c, s) in zip(colors, labels) {
            wedges.append(Wedge(color: c, label: s))
        }

        wedgeView.wedges = wedges

        view.addSubview(wedgeView)
        wedgeView.translatesAutoresizingMaskIntoConstraints = false

        let g = view.safeAreaLayoutGuide

        NSLayoutConstraint.activate([
            wedgeView.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.8),
            wedgeView.heightAnchor.constraint(equalTo: wedgeView.widthAnchor),
            wedgeView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            wedgeView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
        ])

    }

}

上面代码中使用的几个 "helper" 扩展:

// get a random color
extension UIColor {
    static var random: UIColor {
        return UIColor(red: .random(in: 0...1),
                       green: .random(in: 0...1),
                       blue: .random(in: 0...1),
                       alpha: 1.0)
    }
}

// get the point on a circle at specific radian
extension CGPoint {
    static func pointOnCircle(center: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
        let x = center.x + radius * cos(angle)
        let y = center.y + radius * sin(angle)

        return CGPoint(x: x, y: y)
    }
}

// get height of word-wrapping string with max-width
extension String {
    func getLableHeight(_ forWidth: CGFloat, usingFont: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: forWidth, height: .greatestFiniteMagnitude)
        let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: usingFont], context: nil)
        return ceil(boundingBox.height)
    }
}