使用CATextLayers向圆形添加标签

时间:2019-06-20 18:04:48

标签: ios swift uiview

我已经使用CAShapeLayer创建了一个圈子。现在,我想向控件中添加文本,但是我不确定如何这样做(看起来不错)。

我有以下代码:

import Foundation
import UIKit

class Gauge : UIView
{
    private var shapeLayer = CAShapeLayer()
    private var maskingLayer = CAShapeLayer()
    private var gradientLayer = CAGradientLayer()
    private var textLayers: [CATextLayer] = []

    private var mValue: CGFloat = 0.0
    private var mSegments = 9
    private let textHeight: CGFloat = 24.0

    // MARK: Properties
    var lineWidth: CGFloat = 32.0
    var min: CGFloat = 0.0
    var max: CGFloat = 100.0
    var segments: Int
    {
        get { return self.mSegments - 1 }
        set
        {

            self.mSegments = newValue + 1
            self.commonInit()
        }
    }

    var progress: CGFloat
    {
        get
        {
            let diff = abs(self.min) + self.max
            return self.value / diff
        }
    }
    var segmentSize: CGFloat = 270.0
    {
        didSet
        {
            self.value = 0.0

            self.commonInit()
        }
    }

    var value: CGFloat
    {
        get { return self.mValue }
        set
        {
            if self.mValue == newValue { return }

            if newValue < 0.0
            {
                self.mValue = 0.0
            }
            else if newValue > self.max
            {
                self.mValue = self.max
            }
            else
            {
                self.mValue = newValue
            }

            self.maskingLayer.strokeStart = 0.0
            self.maskingLayer.strokeEnd = 0.5
        }
    }

    override init(frame: CGRect)
    {
        super.init(frame: frame)

        self.commonInit()
    }

    required init?(coder aDecoder: NSCoder)
    {
        super.init(coder: aDecoder)

        self.commonInit()
    }

    fileprivate func commonInit()
    {
        self.value = 50

        self.determineLineWidth()
        self.initLayers()
        self.initDataLayers()
        self.initTextLayers()
    }

    override func layoutSubviews()
    {
        super.layoutSubviews()

        self.commonInit()
    }

    fileprivate func initTextLayers()
    {
        for textLayer in self.textLayers
        {
            textLayer.removeFromSuperlayer()
        }

        let fontSize: CGFloat = self.getFontSize()

        for i in 0 ... self.segments
        {
            let orientation = CGFloat(i) * (1.0 / CGFloat(self.segments))
            let span = self.max + abs(self.min)
            let step = span / CGFloat(self.segments)
            let value = CGFloat(i) * step

            let font = UIFont.systemFont(ofSize: fontSize, weight: .bold)
            let width = Utilities.measure(Int(value).description, .zero, font)

            let point = self.getLabelPosition(orientation, width)

            let layer = CATextLayer()
            layer.contentsScale = UIScreen.main.scale
            layer.font = font
            layer.foregroundColor = UIColor.black.cgColor
            layer.fontSize = fontSize
            layer.string = Int(value).description
            layer.alignmentMode = .center

            layer.frame = CGRect(origin: point, size: .init(width: 48.0, height: self.textHeight))

            self.textLayers.append(layer)

            self.layer.addSublayer(layer)
        }
    }

    fileprivate func gaugeFont() -> UIFont
    {
        let valueFontSize = self.getFontSize()

        return UIFont.boldSystemFont(ofSize: valueFontSize)
    }

    fileprivate func getFontSize() -> CGFloat
    {
        if self.bounds.height < 128.0
        {
            return 10.0
        }
        else if self.bounds.height < 256.0
        {
            return 14.0
        }
        else
        {
            return  18.0
        }
    }

    fileprivate func initDataLayers()
    {
        self.maskingLayer.removeFromSuperlayer()

        let fillPath = self.createPath()
        self.maskingLayer.frame = self.bounds
        self.maskingLayer.path = fillPath.cgPath
        self.maskingLayer.lineCap = .round
        self.maskingLayer.fillColor = UIColor.clear.cgColor
        self.maskingLayer.strokeColor = UIColor.black.cgColor
        self.maskingLayer.lineWidth = self.lineWidth / 2.0
        self.maskingLayer.position = CGPoint(x: self.bounds.midX, y: self.bounds.midY)

        self.layer.addSublayer(self.maskingLayer)
    }

    fileprivate func calculateAngle(_ value: CGFloat) -> CGFloat
    {
        let diff = abs(self.min) + self.max

        return value / diff
    }

    fileprivate func getLabelPosition(_ progress: CGFloat, _ width: CGFloat) -> CGPoint
    {
        let size = Swift.min(self.bounds.width - self.lineWidth, self.bounds.height - self.lineWidth)
        let center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
        let alpha = (180.0 - self.segmentSize) / 2.0
        let radius = size / 2.0 - self.lineWidth - width

        let cx = center.x
        let cy = center.y
        let angle = self.segmentSize * progress
        let x2 = self.deg2rad(180.0 + alpha + angle)

        let outerX = cx + (radius + self.lineWidth / 2.0) * CGFloat(cos(x2))
        let outerY = cy + (radius + self.lineWidth / 2.0) * CGFloat(sin(x2))

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

    fileprivate func initLayers()
    {
        self.shapeLayer.removeFromSuperlayer()

        let path = self.createPath()

        self.shapeLayer = CAShapeLayer()
        self.shapeLayer.frame = self.bounds
        self.shapeLayer.path = path.cgPath
        self.shapeLayer.strokeColor = UIColor.lightGray.cgColor
        self.shapeLayer.fillColor = nil
        self.shapeLayer.lineWidth = self.lineWidth / 2.0
        self.shapeLayer.lineCap = .round

        self.layer.addSublayer(self.shapeLayer)
    }

    fileprivate func createPath() -> UIBezierPath
    {
        let size = Swift.min(self.frame.width - self.lineWidth / 2, self.frame.height - self.lineWidth)
        let center = CGPoint(x: self.frame.width / 2.0, y: self.frame.height / 2.0)

        let alpha = (180.0 - self.segmentSize) / 2.0

        let path = UIBezierPath(arcCenter: center, radius: size / 2.0, startAngle: self.deg2rad(180.0 + alpha), endAngle: self.deg2rad(360.0 - alpha), clockwise: true)

        return path
    }

    fileprivate func determineLineWidth()
    {
        if self.bounds.height < 192.0
        {
            self.lineWidth = 20.0
        }
        else if self.bounds.height < 320
        {
            self.lineWidth = 32.0
        }
        else
        {
            self.lineWidth = 40.0
        }
    }

    fileprivate func deg2rad(_ number: CGFloat) -> CGFloat
    {
        return number * .pi / 180
    }
}

结果如下所示: My Result

但是我希望文本像这样完美定位: Desired Result

我尝试手动添加各种偏移量,但是当控件调整大小时,它又开始看起来很糟糕。我可以使用某种公式来计算确切位置吗?

1 个答案:

答案 0 :(得分:2)

看起来getLabelPosition返回了一个点,该点应该用作文本的中心,但是您要将其传递到框架,因此将其用作左上点。

您需要将标签偏移一半,以获取原点。

let size = CGSize(width: 48.0, height: self.textHeight)

var origin = point
origin.x -= size.width / 2
origin.y -= size.height / 2

layer.frame = CGRect(origin: origin, size: size)