inexcitus
inexcitus

Reputation: 2639

Adding labels to a circle using CATextLayers

I have created a circle using a CAShapeLayer. Now I want to add text to the control but I am not quite sure on how to do so (so it looks good).

I have the following code:

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
    }
}

The result looks like this: My Result

But I want the text to be positioned perfectly like this: Desired Result

I tried adding various offsets manually, but when the control gets resized, it started to look bad again. Is there some kind of formula which I can use to calculate the exact position?

Upvotes: 0

Views: 188

Answers (1)

Craig Siemens
Craig Siemens

Reputation: 13266

It looks like getLabelPosition returns a point that should be used as the centre of the text but you're passing it to the frame so it's used as the upper left point.

You need to offset the point by hals the size of the label to get the origin.

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)

Upvotes: 2

Related Questions