swift2 在iOS版Swift中沿着圆形路径绘制文本

ehxuflar  于 2022-11-06  发布在  Swift
关注(0)|答案(7)|浏览(251)

我正在寻找一些关于如何使用Swift2 for iOS9围绕圆的边缘绘制简单的单线字符串的最新帮助/提示。我看到了相当过时的例子,涉及旧的ObjC片段,并且经常仅限于OS X。这甚至可以在iOS中的自定义UIView子类的drawRect()方法中实现吗?

i2loujxw

i2loujxw1#

本来想说“你都试过什么了",但现在是周五下午,我下班早,所以就趁机翻译了一下我的老ObjC代码,这就是了,适合Playground,放在你的UIView里应该是小事一桩。

雨燕2

Swift 3和Swift 4更新见下文...

import UIKit

func centreArcPerpendicularText(str: String, context: CGContextRef, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool){
    //*******************************************************
    // This draws the String str around an arc of radius r,
    // with the text centred at polar angle theta
    //*******************************************************

    let l = str.characters.count
    let attributes = [NSFontAttributeName: font]

    var characters: [String] = [] // This will be an array of single character strings, each character in str
    var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
    var totalArc: CGFloat = 0 // ... and the total arc subtended by the string

    // Calculate the arc subtended by each letter and their total
    for i in 0 ..< l {
        characters += [String(str[str.startIndex.advancedBy(i)])]
        arcs += [chordToArc(characters[i].sizeWithAttributes(attributes).width, radius: r)]
        totalArc += arcs[i]
    }

    // Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
    // or anti-clockwise (right way up at 6 o'clock)?
    let direction: CGFloat = clockwise ? -1 : 1
    let slantCorrection = clockwise ? -CGFloat(M_PI_2) : CGFloat(M_PI_2)

    // The centre of the first character will then be at
    // thetaI = theta - totalArc / 2 + arcs[0] / 2
    // But we add the last term inside the loop
    var thetaI = theta - direction * totalArc / 2

    for i in 0 ..< l {
        thetaI += direction * arcs[i] / 2
        // Call centerText with each character in turn.
        // Remember to add +/-90º to the slantAngle otherwise
        // the characters will "stack" round the arc rather than "text flow"
        centreText(characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection)
        // The centre of the next character will then be at
        // thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
        // but again we leave the last term to the start of the next loop...
        thetaI += direction * arcs[i] / 2
    }
}

func chordToArc(chord: CGFloat, radius: CGFloat) -> CGFloat {
    //*******************************************************
    // Simple geometry
    //*******************************************************
    return 2 * asin(chord / (2 * radius))
}

func centreText(str: String, context: CGContextRef, radius r:CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat) {
    //*******************************************************
    // This draws the String str centred at the position
    // specified by the polar coordinates (r, theta)
    // i.e. the x= r * cos(theta) y= r * sin(theta)
    // and rotated by the angle slantAngle
    //*******************************************************

    // Set the text attributes
    let attributes = [NSForegroundColorAttributeName: c,
        NSFontAttributeName: font]
    // Save the context
    CGContextSaveGState(context)
    // Undo the inversion of the Y-axis (or the text goes backwards!)
    CGContextScaleCTM(context, 1, -1)
    // Move the origin to the centre of the text (negating the y-axis manually)
    CGContextTranslateCTM(context, r * cos(theta), -(r * sin(theta)))
    // Rotate the coordinate system
    CGContextRotateCTM(context, -slantAngle)
    // Calculate the width of the text
    let offset = str.sizeWithAttributes(attributes)
    // Move the origin by half the size of the text
    CGContextTranslateCTM (context, -offset.width / 2, -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
    // Draw the text
    str.drawAtPoint(CGPointZero, withAttributes: attributes)
    // Restore the context
    CGContextRestoreGState(context)
}

//*******************************************************
// Playground code to test
//*******************************************************
let size = CGSize(width: 256, height: 256)

UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
let context = UIGraphicsGetCurrentContext()!
//*******************************************************************
// Scale & translate the context to have 0,0
// at the centre of the screen maths convention
// Obviously change your origin to suit...
//*******************************************************************
CGContextTranslateCTM (context, size.width / 2, size.height / 2)
CGContextScaleCTM (context, 1, -1)

centreArcPerpendicularText("Hello round world", context: context, radius: 100, angle: 0, colour: UIColor.redColor(), font: UIFont.systemFontOfSize(16), clockwise: true)
centreArcPerpendicularText("Anticlockwise", context: context, radius: 100, angle: CGFloat(-M_PI_2), colour: UIColor.redColor(), font: UIFont.systemFontOfSize(16), clockwise: false)
centreText("Hello flat world", context: context, radius: 0, angle: 0 , colour: UIColor.yellowColor(), font: UIFont.systemFontOfSize(16), slantAngle: CGFloat(M_PI_4))

let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

输出为:x1c 0d1x

更新增加了顺时针/逆时针和直线示例。
更新Swift 3

func centreArcPerpendicular(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool){
    //*******************************************************
    // This draws the String str around an arc of radius r,
    // with the text centred at polar angle theta
    //*******************************************************

    let l = str.characters.count
    let attributes = [NSFontAttributeName: font]

    let characters: [String] = str.characters.map { String($0) } // An array of single character strings, each character in str
    var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
    var totalArc: CGFloat = 0 // ... and the total arc subtended by the string

    // Calculate the arc subtended by each letter and their total
    for i in 0 ..< l {
        arcs += [chordToArc(characters[i].size(attributes: attributes).width, radius: r)]
        totalArc += arcs[i]
    }

    // Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
    // or anti-clockwise (right way up at 6 o'clock)?
    let direction: CGFloat = clockwise ? -1 : 1
    let slantCorrection = clockwise ? -CGFloat(M_PI_2) : CGFloat(M_PI_2)

    // The centre of the first character will then be at
    // thetaI = theta - totalArc / 2 + arcs[0] / 2
    // But we add the last term inside the loop
    var thetaI = theta - direction * totalArc / 2

    for i in 0 ..< l {
        thetaI += direction * arcs[i] / 2
        // Call centerText with each character in turn.
        // Remember to add +/-90º to the slantAngle otherwise
        // the characters will "stack" round the arc rather than "text flow"
        centre(text: characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection)
        // The centre of the next character will then be at
        // thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
        // but again we leave the last term to the start of the next loop...
        thetaI += direction * arcs[i] / 2
    }
}

func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
    //*******************************************************
    // Simple geometry
    //*******************************************************
    return 2 * asin(chord / (2 * radius))
}

func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat) {
    //*******************************************************
    // This draws the String str centred at the position
    // specified by the polar coordinates (r, theta)
    // i.e. the x= r * cos(theta) y= r * sin(theta)
    // and rotated by the angle slantAngle
    //*******************************************************

    // Set the text attributes
    let attributes = [NSForegroundColorAttributeName: c,
                      NSFontAttributeName: font]
    // Save the context
    context.saveGState()
    // Undo the inversion of the Y-axis (or the text goes backwards!)
    context.scaleBy(x: 1, y: -1)
    // Move the origin to the centre of the text (negating the y-axis manually)
    context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
    // Rotate the coordinate system
    context.rotate(by: -slantAngle)
    // Calculate the width of the text
    let offset = str.size(attributes: attributes)
    // Move the origin by half the size of the text
    context.translateBy (x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
    // Draw the text
    str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
    // Restore the context
    context.restoreGState()
}

//*******************************************************
// Playground code to test
//*******************************************************
let size = CGSize(width: 256, height: 256)

UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
let context = UIGraphicsGetCurrentContext()!
//*******************************************************************
// Scale & translate the context to have 0,0
// at the centre of the screen maths convention
// Obviously change your origin to suit...
//*******************************************************************
context.translateBy (x: size.width / 2, y: size.height / 2)
context.scaleBy (x: 1, y: -1)

centreArcPerpendicular(text: "Hello round world", context: context, radius: 100, angle: 0, colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: true)
centreArcPerpendicular(text: "Anticlockwise", context: context, radius: 100, angle: CGFloat(-M_PI_2), colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: false)
centre(text: "Hello flat world", context: context, radius: 0, angle: 0 , colour: UIColor.yellow, font: UIFont.systemFont(ofSize: 16), slantAngle: CGFloat(M_PI_4))

let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

雨燕4

再一次,小的变化,这一次修复了M_PI的弃用,String放弃了.characters.size(withAttributes...中的参数标签的变化,以及NSAttributedStringKey枚举的文本属性的变化。

import UIKit

func centreArcPerpendicular(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool){
    //*******************************************************
    // This draws the String str around an arc of radius r,
    // with the text centred at polar angle theta
    //*******************************************************

    let characters: [String] = str.map { String($0) } // An array of single character strings, each character in str
    let l = characters.count
    let attributes = [NSAttributedStringKey.font: font]

    var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
    var totalArc: CGFloat = 0 // ... and the total arc subtended by the string

    // Calculate the arc subtended by each letter and their total
    for i in 0 ..< l {
        arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: r)]
        totalArc += arcs[i]
    }

    // Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
    // or anti-clockwise (right way up at 6 o'clock)?
    let direction: CGFloat = clockwise ? -1 : 1
    let slantCorrection: CGFloat = clockwise ? -.pi / 2 : .pi / 2

    // The centre of the first character will then be at
    // thetaI = theta - totalArc / 2 + arcs[0] / 2
    // But we add the last term inside the loop
    var thetaI = theta - direction * totalArc / 2

    for i in 0 ..< l {
        thetaI += direction * arcs[i] / 2
        // Call centerText with each character in turn.
        // Remember to add +/-90º to the slantAngle otherwise
        // the characters will "stack" round the arc rather than "text flow"
        centre(text: characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection)
        // The centre of the next character will then be at
        // thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
        // but again we leave the last term to the start of the next loop...
        thetaI += direction * arcs[i] / 2
    }
}

func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
    //*******************************************************
    // Simple geometry
    //*******************************************************
    return 2 * asin(chord / (2 * radius))
}

func centre(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat) {
    //*******************************************************
    // This draws the String str centred at the position
    // specified by the polar coordinates (r, theta)
    // i.e. the x= r * cos(theta) y= r * sin(theta)
    // and rotated by the angle slantAngle
    //*******************************************************

    // Set the text attributes
    let attributes = [NSAttributedStringKey.foregroundColor: c, NSAttributedStringKey.font: font]
    //let attributes = [NSForegroundColorAttributeName: c, NSFontAttributeName: font]
    // Save the context
    context.saveGState()
    // Undo the inversion of the Y-axis (or the text goes backwards!)
    context.scaleBy(x: 1, y: -1)
    // Move the origin to the centre of the text (negating the y-axis manually)
    context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
    // Rotate the coordinate system
    context.rotate(by: -slantAngle)
    // Calculate the width of the text
    let offset = str.size(withAttributes: attributes)
    // Move the origin by half the size of the text
    context.translateBy (x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
    // Draw the text
    str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
    // Restore the context
    context.restoreGState()
}

//*******************************************************
// Playground code to test
//*******************************************************
let size = CGSize(width: 256, height: 256)

UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
let context = UIGraphicsGetCurrentContext()!
//*******************************************************************
// Scale & translate the context to have 0,0
// at the centre of the screen maths convention
// Obviously change your origin to suit...
//*******************************************************************
context.translateBy (x: size.width / 2, y: size.height / 2)
context.scaleBy(x: 1, y: -1)

centreArcPerpendicular(text: "Hello round 🌏 world", context: context, radius: 100, angle: 0, colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: true)
centreArcPerpendicular(text: "Anticlockwise", context: context, radius: 100, angle: CGFloat(-M_PI_2), colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: false)
centre(text: "Hello flat world", context: context, radius: 0, angle: 0 , colour: UIColor.yellow, font: UIFont.systemFont(ofSize: 16), slantAngle: .pi / 4)

let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

更新以显示在UIView中的使用

评论员@RitvikUpadhyaya问如何在UIView中实现这一点--这对老手来说是显而易见的,但对初学者来说可能就不那么容易了。诀窍是使用UIGraphicsGetCurrentContext * 而不 * 调用UIGraphicsBeginImageContextWithOptions(这会将UIView的上下文覆盖为 * 当前 * 上下文)来获得正确的上下文--因此你的UIView应该看起来像这样:

class MyView: UIView {
    override func draw(_ rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext() else { return }
        let size = self.bounds.size

        context.translateBy (x: size.width / 2, y: size.height / 2)
        context.scaleBy (x: 1, y: -1)

        centreArcPerpendicular(text: "Hello round world", context: context, radius: 100, angle: 0, colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: true)
        centreArcPerpendicular(text: "Anticlockwise", context: context, radius: 100, angle: CGFloat(-M_PI_2), colour: UIColor.red, font: UIFont.systemFont(ofSize: 16), clockwise: false)
        centre(text: "Hello flat world", context: context, radius: 0, angle: 0 , colour: UIColor.yellow, font: UIFont.systemFont(ofSize: 16), slantAngle: CGFloat(M_PI_4))
    }
}
hivapdat

hivapdat2#

@ IB可用于循环路径上的UI标签

首先,我想我们都同意@Grimxn是那个人!他的解决方案很棒。我把他的工作和重构成一个自定义的UILabel控件,你可以在故事板上设置和编辑。如果你们看过我的视频,你会知道我有多喜欢做这些事情!😀

自定义UI标签的Swift 3代码

import UIKit

@IBDesignable
class UILabelX: UILabel {
    //*******************************************************
    // DEFINITIONS (Because I'm not brilliant and I'll forget most this tomorrow.)
    // Radius: A straight line from the center to the circumference of a circle.
    // Circumference: The distance around the edge (outer line) the circle.
    // Arc: A part of the circumference of a circle. Like a length or section of the circumference.
    // Theta: A label or name that represents an angle.
    // Subtend: A letter has a width. If you put the letter on the circumference, the letter's width
    //          gives you an arc. So now that you have an arc (a length on the circumference) you can
    //          use that to get an angle. You get an angle when you draw a line from the center of the
    //          circle to each end point of your arc. So "subtend" means to get an angle from an arc.
    // Chord: A line segment connecting two points on a curve. If you have an arc then there is a
    //          start point and an end point. If you draw a straight line from start point to end point
    //          then you have a "chord".
    // sin: (Super simple/incomplete definition) Or "sine" takes an angle in degrees and gives you a number.
    // asin: Or "asine" takes a number and gives you an angle in degrees. Opposite of sine.
    //          More complete definition: http://www.mathsisfun.com/sine-cosine-tangent.html
    // cosine: Also takes an angle in degrees and gives you another number from using the two radiuses (radii).
    //*******************************************************

    @IBInspectable var angle: CGFloat = 1.6
    @IBInspectable var clockwise: Bool = true

    override func draw(_ rect: CGRect) {
        centreArcPerpendicular()
    }

    /**
     This draws the self.text around an arc of radius r,
     with the text centred at polar angle theta
     */
    func centreArcPerpendicular() {
        guard let context = UIGraphicsGetCurrentContext() else { return }
        let str = self.text ?? ""
        let size = self.bounds.size
        context.translateBy(x: size.width / 2, y: size.height / 2)

        let radius = getRadiusForLabel()
        let l = str.characters.count
        let attributes: [String : Any] = [NSFontAttributeName: self.font]

        let characters: [String] = str.characters.map { String($0) } // An array of single character strings, each character in str
        var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
        var totalArc: CGFloat = 0 // ... and the total arc subtended by the string

        // Calculate the arc subtended by each letter and their total
        for i in 0 ..< l {
            arcs += [chordToArc(characters[i].size(attributes: attributes).width, radius: radius)]
            totalArc += arcs[i]
        }

        // Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
        // or anti-clockwise (right way up at 6 o'clock)?
        let direction: CGFloat = clockwise ? -1 : 1
        let slantCorrection = clockwise ? -CGFloat(M_PI_2) : CGFloat(M_PI_2)

        // The centre of the first character will then be at
        // thetaI = theta - totalArc / 2 + arcs[0] / 2
        // But we add the last term inside the loop
        var thetaI = angle - direction * totalArc / 2

        for i in 0 ..< l {
            thetaI += direction * arcs[i] / 2
            // Call centre with each character in turn.
            // Remember to add +/-90º to the slantAngle otherwise
            // the characters will "stack" round the arc rather than "text flow"
            centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
            // The centre of the next character will then be at
            // thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
            // but again we leave the last term to the start of the next loop...
            thetaI += direction * arcs[i] / 2
        }
    }

    func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
        //*******************************************************
        // Simple geometry
        //*******************************************************
        return 2 * asin(chord / (2 * radius))
    }

    /**
     This draws the String str centred at the position
     specified by the polar coordinates (r, theta)
     i.e. the x= r * cos(theta) y= r * sin(theta)
     and rotated by the angle slantAngle
    */
    func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
        // Set the text attributes
        let attributes = [NSForegroundColorAttributeName: self.textColor,
                          NSFontAttributeName: self.font] as [String : Any]
        // Save the context
        context.saveGState()
        // Move the origin to the centre of the text (negating the y-axis manually)
        context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
        // Rotate the coordinate system
        context.rotate(by: -slantAngle)
        // Calculate the width of the text
        let offset = str.size(attributes: attributes)
        // Move the origin by half the size of the text
        context.translateBy(x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
        // Draw the text
        str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
        // Restore the context
        context.restoreGState()
    }

    func getRadiusForLabel() -> CGFloat {
        // Imagine the bounds of this label will have a circle inside it.
        // The circle will be as big as the smallest width or height of this label.
        // But we need to fit the size of the font on the circle so make the circle a little
        // smaller so the text does not get drawn outside the bounds of the circle.
        let smallestWidthOrHeight = min(self.bounds.size.height, self.bounds.size.width)
        let heightOfFont = self.text?.size(attributes: [NSFontAttributeName: self.font]).height ?? 0

        // Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
        return (smallestWidthOrHeight/2) - heightOfFont + 5
    }
}

情节提要上的用法示例

我所做的更改

  • 我删除了可以直接从标签中获取的参数。
  • 我承认我不是三角学中最聪明的,在我这个年龄已经忘记了很多,所以我包括了所有相关的定义,这样我就可以开始理解@Grimxn brilliance。
  • 现在,"Angular“和”顺时针“设置是可以在”属性检查器“中调整的特性。
  • 我现在从标示的大小建立半径。
  • 把一些注解以标准格式放在函数上,你知道,这样你就可以得到弹出菜单,弹出你的OPTION + CLICK函数。

我遇到的问题

我鼓励你编辑上面的内容来改进它。

  • 我不知道为什么,但有时标签总是呈现在其他控件上,即使它在文档大纲中位于其他控件的后面。
oxf4rvwz

oxf4rvwz3#

始终采用相同的实施方式,但针对Swift 4进行了调整

import UIKit

@IBDesignable
class CircularLabel: UILabel {
    //*******************************************************
    // DEFINITIONS (Because I'm not brilliant and I'll forget most this tomorrow.)
    // Radius: A straight line from the center to the circumference of a circle.
    // Circumference: The distance around the edge (outer line) the circle.
    // Arc: A part of the circumference of a circle. Like a length or section of the circumference.
    // Theta: A label or name that represents an angle.
    // Subtend: A letter has a width. If you put the letter on the circumference, the letter's width
    //          gives you an arc. So now that you have an arc (a length on the circumference) you can
    //          use that to get an angle. You get an angle when you draw a line from the center of the
    //          circle to each end point of your arc. So "subtend" means to get an angle from an arc.
    // Chord: A line segment connecting two points on a curve. If you have an arc then there is a
    //          start point and an end point. If you draw a straight line from start point to end point
    //          then you have a "chord".
    // sin: (Super simple/incomplete definition) Or "sine" takes an angle in degrees and gives you a number.
    // asin: Or "asine" takes a number and gives you an angle in degrees. Opposite of sine.
    //          More complete definition: http://www.mathsisfun.com/sine-cosine-tangent.html
    // cosine: Also takes an angle in degrees and gives you another number from using the two radiuses (radii).
    //*******************************************************

    @IBInspectable var angle: CGFloat = 1.6
    @IBInspectable var clockwise: Bool = true

    override func draw(_ rect: CGRect) {
        centreArcPerpendicular()
    }
    /**
    This draws the self.text around an arc of radius r,
    with the text centred at polar angle theta
    */
    func centreArcPerpendicular() {
        guard let context = UIGraphicsGetCurrentContext() else { return }
        let string = text ?? ""
        let size   = bounds.size
        context.translateBy(x: size.width / 2, y: size.height / 2)

        let radius = getRadiusForLabel()
        let l = string.count
        let attributes = [NSAttributedStringKey.font : self.font!]

        let characters: [String] = string.map { String($0) } // An array of single character strings, each character in str
        var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
        var totalArc: CGFloat = 0 // ... and the total arc subtended by the string

        // Calculate the arc subtended by each letter and their total
        for i in 0 ..< l {
            arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: radius)]
            totalArc += arcs[i]
        }

        // Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
        // or anti-clockwise (right way up at 6 o'clock)?
        let direction: CGFloat = clockwise ? -1 : 1
        let slantCorrection = clockwise ? -CGFloat.pi/2 : CGFloat.pi/2

        // The centre of the first character will then be at
        // thetaI = theta - totalArc / 2 + arcs[0] / 2
        // But we add the last term inside the loop
        var thetaI = angle - direction * totalArc / 2

        for i in 0 ..< l {
            thetaI += direction * arcs[i] / 2
            // Call centre with each character in turn.
            // Remember to add +/-90º to the slantAngle otherwise
            // the characters will "stack" round the arc rather than "text flow"
            centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
            // The centre of the next character will then be at
            // thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
            // but again we leave the last term to the start of the next loop...
            thetaI += direction * arcs[i] / 2
        }
    }

    func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
        //*******************************************************
        // Simple geometry
        //*******************************************************
        return 2 * asin(chord / (2 * radius))
    }

    /**
    This draws the String str centred at the position
    specified by the polar coordinates (r, theta)
    i.e. the x= r * cos(theta) y= r * sin(theta)
    and rotated by the angle slantAngle
    */
    func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
        // Set the text attributes
        let attributes : [NSAttributedStringKey : Any] = [
            NSAttributedStringKey.foregroundColor: textColor!,
            NSAttributedStringKey.font: font!
            ]
        // Save the context
        context.saveGState()
        // Move the origin to the centre of the text (negating the y-axis manually)
        context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
        // Rotate the coordinate system
        context.rotate(by: -slantAngle)
        // Calculate the width of the text
        let offset = str.size(withAttributes: attributes)
        // Move the origin by half the size of the text
        context.translateBy(x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
        // Draw the text
        str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
        // Restore the context
        context.restoreGState()
    }

    func getRadiusForLabel() -> CGFloat {
        // Imagine the bounds of this label will have a circle inside it.
        // The circle will be as big as the smallest width or height of this label.
        // But we need to fit the size of the font on the circle so make the circle a little
        // smaller so the text does not get drawn outside the bounds of the circle.
        let smallestWidthOrHeight = min(bounds.size.height, bounds.size.width)
        let heightOfFont = text?.size(withAttributes: [NSAttributedStringKey.font: self.font]).height ?? 0

        // Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
        return (smallestWidthOrHeight/2) - heightOfFont + 5
    }
}
xmd2e60i

xmd2e60i4#

@可为Swift 2循环路径上的UI标签设计

非常感谢@Grimxn和@mark-moeykens的工作。我对Mark的工作做了一个小的重构,这样我就可以在一个还没有花时间更新到Swift 3的项目中使用它。我想分享一下,因为以前的帖子很有帮助。

自定义UI标签的Swift 2代码

import UIKit

@IBDesignable
class ArcUILabel: UILabel
{
    //*******************************************************
    // DEFINITIONS (Because I'm not brilliant and I'll forget most this tomorrow.)
    // Radius: A straight line from the center to the circumference of a circle.
    // Circumference: The distance around the edge (outer line) the circle.
    // Arc: A part of the circumference of a circle. Like a length or section of the circumference.
    // Theta: A label or name that represents an angle.
    // Subtend: A letter has a width. If you put the letter on the circumference, the letter's width
    //          gives you an arc. So now that you have an arc (a length on the circumference) you can
    //          use that to get an angle. You get an angle when you draw a line from the center of the
    //          circle to each end point of your arc. So "subtend" means to get an angle from an arc.
    // Chord: A line segment connecting two points on a curve. If you have an arc then there is a
    //          start point and an end point. If you draw a straight line from start point to end point
    //          then you have a "chord".
    // sin: (Super simple/incomplete definition) Or "sine" takes an angle in degrees and gives you a number.
    // asin: Or "asine" takes a number and gives you an angle in degrees. Opposite of sine.
    //          More complete definition: http://www.mathsisfun.com/sine-cosine-tangent.html
    // cosine: Also takes an angle in degrees and gives you another number from using the two radiuses (radii).
    //*******************************************************

    @IBInspectable var angle: CGFloat = 1.6
    @IBInspectable var clockwise: Bool = true

    override func drawRect(rect: CGRect)
    {
        centreArcPerpendicular()
    }

    /**
     This draws the self.text around an arc of radius r,
     with the text centred at polar angle theta
     */
    func centreArcPerpendicular() {
        guard let context = UIGraphicsGetCurrentContext() else { return }
        let str = self.text ?? ""
        let size = self.bounds.size
        CGContextTranslateCTM(context, size.width / 2, size.height / 2)

        let radius = getRadiusForLabel()
        let l = str.characters.count
        let attributes: [String : AnyObject] = [NSFontAttributeName: self.font]

        let characters: [String] = str.characters.map { String($0) } // An array of single character strings, each character in str
        var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
        var totalArc: CGFloat = 0 // ... and the total arc subtended by the string

        // Calculate the arc subtended by each letter and their total
        for i in 0 ..< l {
            arcs += [chordToArc(characters[i].sizeWithAttributes(attributes).width, radius: radius)]
            totalArc += arcs[i]
        }

        // Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
        // or anti-clockwise (right way up at 6 o'clock)?
        let direction: CGFloat = clockwise ? -1 : 1
        let slantCorrection = clockwise ? -CGFloat(M_PI_2) : CGFloat(M_PI_2)

        // The centre of the first character will then be at
        // thetaI = theta - totalArc / 2 + arcs[0] / 2
        // But we add the last term inside the loop
        var thetaI = angle - direction * totalArc / 2

        for i in 0 ..< l {
            thetaI += direction * arcs[i] / 2
            // Call centre with each character in turn.
            // Remember to add +/-90º to the slantAngle otherwise
            // the characters will "stack" round the arc rather than "text flow"
            centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
            // The centre of the next character will then be at
            // thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
            // but again we leave the last term to the start of the next loop...
            thetaI += direction * arcs[i] / 2
        }
    }

    func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
        //*******************************************************
        // Simple geometry
        //*******************************************************
        return 2 * asin(chord / (2 * radius))
    }

    /**
     This draws the String str centred at the position
     specified by the polar coordinates (r, theta)
     i.e. the x= r * cos(theta) y= r * sin(theta)
     and rotated by the angle slantAngle
     */
    func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
        // Set the text attributes
        let attributes = [NSForegroundColorAttributeName: self.textColor,
                          NSFontAttributeName: self.font] as [String : AnyObject]
        // Save the context
        CGContextSaveGState(context)
        // Move the origin to the centre of the text (negating the y-axis manually)
        CGContextTranslateCTM(context, r * cos(theta), -(r * sin(theta)))
        // Rotate the coordinate system
        CGContextRotateCTM(context, -slantAngle)

        // Calculate the width of the text
        let offset: CGSize = str.sizeWithAttributes(attributes)
        // Move the origin by half the size of the text
        CGContextTranslateCTM(context, -offset.width / 2, -offset.height / 2)

        // Draw the text
        let txtStr = NSString(string: str)
        txtStr.drawAtPoint(CGPoint(x: 0, y: 0), withAttributes: attributes)

        // Restore the context
        CGContextRestoreGState(context)
    }

    func getRadiusForLabel() -> CGFloat {
        // Imagine the bounds of this label will have a circle inside it.
        // The circle will be as big as the smallest width or height of this label.
        // But we need to fit the size of the font on the circle so make the circle a little
        // smaller so the text does not get drawn outside the bounds of the circle.
        let smallestWidthOrHeight = min(self.bounds.size.height, self.bounds.size.width)
        let heightOfFont = self.text?.sizeWithAttributes([NSFontAttributeName: self.font]).height ?? 0

        // Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
        return (smallestWidthOrHeight/2) - heightOfFont + 5
    }
}
6qfn3psc

6qfn3psc5#

基于Grimxm代码的C#版本:

private void CenterArcPerpendicular(string text, CGContext context, float radius, double angle, UIColor textColor, UIFont font, bool isClockwise) {
    var characters = text.ToCharArray();

    var arcs = new List<float>() { };
    float totalArc = 0;

    for (var i = 0; i < characters.Length; i++)
    {
        var character = new NSString(new string(new char[] { characters[i] }));
        var charSize = character.StringSize(font);
        var arc = ChordToArc((float)charSize.Width, radius);
        arcs.Add(arc);

        totalArc += arc;
    }

    var direction = isClockwise ? -1 : 1;
    var slantCorrection = (float)(isClockwise ? -(Math.PI / 2) : (Math.PI / 2));
    var thetaI = angle - (direction * (totalArc / 2));

    for (var i = 0; i < characters.Length; i++)
    {
        var character = new NSString(new string(new char[] { characters[i] }));
        thetaI += direction * arcs[i] / 2;
        CenterText(character, context: context, radius: radius, angle: thetaI, textColor: textColor, font: font, slantAngle: thetaI + slantCorrection);
        thetaI += direction * arcs[i] / 2;
    }
}

private float ChordToArc(float chord, float radius) {
    return (float)(2 * Math.Asin(chord / (2 * radius)));
}

private void CenterText(NSString text, CGContext context, float radius, double angle, UIColor textColor, UIFont font, double slantAngle)
{
    var attributes = new UIStringAttributes { Font = font, ForegroundColor = textColor };

    context.SaveState();
    context.ScaleCTM(1, -1);
    var dX = radius * Math.Cos(angle);
    var dY = -(radius * Math.Sin(angle));

    context.TranslateCTM((nfloat)dX, (nfloat)dY);
    context.RotateCTM(-(nfloat)slantAngle);

    var offset = text.StringSize(font);
    context.TranslateCTM(-offset.Width / 2, -offset.Height / 2);

    text.DrawString(CGPoint.Empty, attributes);
    context.RestoreState();
}

适用于Xamarin iOS应用程序。

83qze16e

83qze16e6#

将代码更新为Swift 5

import Foundation
import UIKit
 @IBDesignable       
class UILabelX: UILabel {

@IBInspectable var angle: CGFloat = 1.6
@IBInspectable var clockwise: Bool = true

override func draw(_ rect: CGRect) {
    centreArcPerpendicular()
}

/**
 This draws the self.text around an arc of radius r,
 with the text centred at polar angle theta
 */
func centreArcPerpendicular() {
    guard let context = UIGraphicsGetCurrentContext() else { return }
    let str = self.text ?? ""
    let size = self.bounds.size
    context.translateBy(x: size.width / 2, y: size.height / 2)

    let radius = getRadiusForLabel()
    let l = str.count
//        let attributes: [String : Any] = [NSAttributedString.Key: self.font]
    let attributes : [NSAttributedString.Key : Any] = [.font : self.font]

    let characters: [String] = str.map { String($0) } // An array of single character strings, each character in str
    var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
    var totalArc: CGFloat = 0 // ... and the total arc subtended by the string

    // Calculate the arc subtended by each letter and their total
    for i in 0 ..< l {
 //            arcs = [chordToArc(characters[i].widthOfString(usingFont: self.font), radius: radius)]
        arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: radius)]
        totalArc += arcs[i]
    }

    // Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
    // or anti-clockwise (right way up at 6 o'clock)?
    let direction: CGFloat = clockwise ? -1 : 1
    let slantCorrection = clockwise ? -CGFloat(Double.pi/2) : CGFloat(Double.pi/2)

    // The centre of the first character will then be at
    // thetaI = theta - totalArc / 2 + arcs[0] / 2
    // But we add the last term inside the loop
    var thetaI = angle - direction * totalArc / 2

         for i in 0 ..< l {
        thetaI += direction * arcs[i] / 2
        // Call centre with each character in turn.
        // Remember to add +/-90º to the slantAngle otherwise
        // the characters will "stack" round the arc rather than "text flow"
        centre(text: characters[i], context: context, radius: radius, angle: thetaI, slantAngle: thetaI + slantCorrection)
        // The centre of the next character will then be at
        // thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
        // but again we leave the last term to the start of the next loop...
        thetaI += direction * arcs[i] / 2
    }
}

func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
    //*******************************************************
    // Simple geometry
    //*******************************************************
    return 2 * asin(chord / (2 * radius))
}

/**
 This draws the String str centred at the position
 specified by the polar coordinates (r, theta)
 i.e. the x= r * cos(theta) y= r * sin(theta)
 and rotated by the angle slantAngle

* /

func centre(text str: String, context: CGContext, radius r:CGFloat, angle theta: CGFloat, slantAngle: CGFloat) {
    // Set the text attributes
    let attributes = [NSAttributedString.Key.font: self.font!] as [NSAttributedString.Key : Any]
    // Save the context
    context.saveGState()
    // Move the origin to the centre of the text (negating the y-axis manually)
    context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
    // Rotate the coordinate system
    context.rotate(by: -slantAngle)
    // Calculate the width of the text
    let offset = str.size(withAttributes: attributes)
    // Move the origin by half the size of the text
    context.translateBy(x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
    // Draw the text
    str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
    // Restore the context
    context.restoreGState()
}

func getRadiusForLabel() -> CGFloat {
    // Imagine the bounds of this label will have a circle inside it.
    // The circle will be as big as the smallest width or height of this label.
    // But we need to fit the size of the font on the circle so make the circle a little
    // smaller so the text does not get drawn outside the bounds of the circle.
    let smallestWidthOrHeight = min(self.bounds.size.height, self.bounds.size.width)
    let heightOfFont = self.text?.size(withAttributes: [NSAttributedString.Key.font: self.font]).height ?? 0

    // Dividing the smallestWidthOrHeight by 2 gives us the radius for the circle.
    return (smallestWidthOrHeight/2) - heightOfFont + 5
}
}
v6ylcynt

v6ylcynt7#

  • 雨燕5
  • 透明背景
  • 文字的字距克恩选项
  • 视图的正确比率

完整的Playground代码

import UIKit
import PlaygroundSupport

class MyViewController : UIViewController {

    override func loadView() {

        //*******************************************************
        // Playground code to test
        //*******************************************************
        let size = CGSize(width: 256, height: 256)

        UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
        let context = UIGraphicsGetCurrentContext()!
        //*******************************************************************
        // Scale & translate the context to have 0,0
        // at the centre of the screen maths convention
        // Obviously change your origin to suit...
        //*******************************************************************
        context.translateBy (x: size.width / 2, y: size.height / 2)
        context.scaleBy(x: 1, y: -1)

        centreArcPerpendicular(text: "Hello round World",
                               context: context,
                               radius: size.height * 0.44,
                               angle: 0,
                               colour: UIColor.white,
                               font: UIFont.systemFont(ofSize: 16),
                               clockwise: true,
                               kern: 3)
        centreArcPerpendicular(text: "Anticlockwise",
                               context: context,
                               radius: size.height * 0.44,
                               angle: -.pi,
                               colour: UIColor.white,
                               font: UIFont.systemFont(ofSize: 16),
                               clockwise: false,
                               kern: 3)
        centre(text: "Hello center world", context: context, radius: 0, angle: 0 , colour: UIColor.yellow, font: UIFont.systemFont(ofSize: 16), slantAngle: .pi/4, kern: 0)

        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
    }

    func centreArcPerpendicular(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, clockwise: Bool, kern: CGFloat = 0) {

        //*******************************************************
        // This draws the String str around an arc of radius r,
        // with the text centred at polar angle theta
        //*******************************************************

        func chordToArc(_ chord: CGFloat, radius: CGFloat) -> CGFloat {
            return 2 * asin(chord / (2 * radius))
        }

        let characters: [String] = str.map { String($0) } // An array of single character strings, each character in str
        let l = characters.count
        let attributes = [NSAttributedString.Key.font: font,
                          NSAttributedString.Key.kern: kern] as [NSAttributedString.Key : Any]

        var arcs: [CGFloat] = [] // This will be the arcs subtended by each character
        var totalArc: CGFloat = 0 // ... and the total arc subtended by the string

        // Calculate the arc subtended by each letter and their total
        for i in 0 ..< l {
            arcs += [chordToArc(characters[i].size(withAttributes: attributes).width, radius: r)]
            totalArc += arcs[i]
        }

        // Are we writing clockwise (right way up at 12 o'clock, upside down at 6 o'clock)
        // or anti-clockwise (right way up at 6 o'clock)?
        let direction: CGFloat = clockwise ? -1 : 1
        let slantCorrection: CGFloat = clockwise ? -.pi / 2 : .pi / 2

        // The centre of the first character will then be at
        // thetaI = theta - totalArc / 2 + arcs[0] / 2
        // But we add the last term inside the loop
        var thetaI = theta - direction * totalArc / 2

        for i in 0 ..< l {
            thetaI += direction * arcs[i] / 2
            // Call centerText with each character in turn.
            // Remember to add +/-90º to the slantAngle otherwise
            // the characters will "stack" round the arc rather than "text flow"
            centre(text: characters[i], context: context, radius: r, angle: thetaI, colour: c, font: font, slantAngle: thetaI + slantCorrection, kern: kern)
            // The centre of the next character will then be at
            // thetaI = thetaI + arcs[i] / 2 + arcs[i + 1] / 2
            // but again we leave the last term to the start of the next loop...
            thetaI += direction * arcs[i] / 2
        }
    }

    func centre(text str: String, context: CGContext, radius r: CGFloat, angle theta: CGFloat, colour c: UIColor, font: UIFont, slantAngle: CGFloat, kern: CGFloat) {
        //*******************************************************
        // This draws the String str centred at the position
        // specified by the polar coordinates (r, theta)
        // i.e. the x= r * cos(theta) y= r * sin(theta)
        // and rotated by the angle slantAngle
        //*******************************************************

        // Set the text attributes
        let attributes = [NSAttributedString.Key.foregroundColor: c,
                          NSAttributedString.Key.font: font,
                          NSAttributedString.Key.kern: kern] as [NSAttributedString.Key : Any]

        //let attributes = [NSForegroundColorAttributeName: c, NSFontAttributeName: font]
        // Save the context
        context.saveGState()
        // Undo the inversion of the Y-axis (or the text goes backwards!)
        context.scaleBy(x: 1, y: -1)
        // Move the origin to the centre of the text (negating the y-axis manually)
        context.translateBy(x: r * cos(theta), y: -(r * sin(theta)))
        // Rotate the coordinate system
        context.rotate(by: -slantAngle)
        // Calculate the width of the text
        let offset = str.size(withAttributes: attributes)
        // Move the origin by half the size of the text
        context.translateBy (x: -offset.width / 2, y: -offset.height / 2) // Move the origin to the centre of the text (negating the y-axis manually)
        // Draw the text
        str.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
        // Restore the context
        context.restoreGState()
    }
}

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

相关问题