如何在CATextLayer中垂直对齐文本?

29

我正在开发一个CATextLayer,希望它可以在Mac和iOS上使用。我能控制图层内文本的垂直对齐吗?

在这种情况下,我想要将文本垂直居中--但其他垂直对齐方式的信息也很有用。

编辑:我找到了这个,但是我无法使其工作。

16个回答

33

正如您已经发现的那样,正确答案在Objective-C中这里,并且适用于iOS。 它通过对CATextLayer进行子类化并重写drawInContext函数来实现。

然而,我对代码进行了一些改进,如下所示,使用David Hoerl的代码作为基础。 更改仅涉及重新计算由yDiff表示的文本的垂直位置。 我已经用自己的代码进行了测试。

以下是Swift用户的代码:

class LCTextLayer : CATextLayer {

    // REF: http://lists.apple.com/archives/quartz-dev/2008/Aug/msg00016.html
    // CREDIT: David Hoerl - https://github.com/dhoerl 
    // USAGE: To fix the vertical alignment issue that currently exists within the CATextLayer class. Change made to the yDiff calculation.

    override func draw(in context: CGContext) {
        let height = self.bounds.size.height
        let fontSize = self.fontSize
        let yDiff = (height-fontSize)/2 - fontSize/10

        context.saveGState()
        context.translateBy(x: 0, y: yDiff) // Use -yDiff when in non-flipped coordinates (like macOS's default)
        super.draw(in: context)
        context.restoreGState()
    }
}

5
你的方法对于单行文本效果很好,但是对于多行文本(isWrapped = true),你需要先计算文本的行数,然后更新 yDiff = (height-lines*fontSize)/2 - lines*fontSize/10 - Tai Le

15
这是一个晚回答,但我最近也有同样的问题,并通过以下调查解决了问题。
垂直对齐取决于您需要绘制的文本和您正在使用的字体,因此没有一种通用的解决方案可以使其在所有情况下垂直。但是,我们仍然可以计算不同情况下的垂直中点。
根据苹果公司的About Text Handling in iOS,我们需要知道文本的绘制方式。例如,我正在尝试为工作日字符串(Sun、Mon、Tue等)进行垂直对齐。
对于这种情况,文本的高度取决于cap Height,并且这些字符没有descent。因此,如果我们需要将这些文本垂直居中,我们可以计算大写字符顶部的偏移量,例如字符“S”的顶部位置。
根据下面的图示:

fig

顶部空间用于大写字母"S"应为

font.ascender - font.capHeight

底部空间用于大写字母"S"将会是

font.descender + font.leading

所以我们需要通过以下方式稍微将"S"向下移动一点:

y = (font.ascender - font.capHeight + font.descender + font.leading + font.capHeight) / 2

那等于:

y = (font.ascender + font.descender + font.leading) / 2

然后我可以使文本垂直居中对齐。
结论:
  1. If your text does not include any character exceed the baseline, e.g. "p", "j", "g", and no character over the top of cap height, e.g. "f". The you can use the formula above to make the text align vertical.

    y = (font.ascender + font.descender + font.leading) / 2
    
  2. If your text include character below the baseline, e.g. "p", "j", and no character exceed the top of cap height, e.g. "f". Then the vertical formula would be:

    y = (font.ascender + font.descender) / 2
    
  3. If your text include does not include character drawn below the baseline, e.g. "j", "p", and does include character drawn above the cap height line, e.g. "f". Then y would be:

    y = (font.descender + font.leading) / 2
    
  4. If all characters would be occurred in your text, then y equals to:

    y = font.leading / 2
    

2
y 的意思是什么? - alpine
1
@alpine 我认为是frame/bounds的origin.y - Kjuly

13

可能答案有些晚了,不过您可以计算文本大小,然后设置文本图层的位置。同时,您需要将文本图层的文本对齐模式设置为“中心”。

CGRect labelRect = [text boundingRectWithSize:view.bounds.size options:NSStringDrawingUsesLineFragmentOrigin attributes:@{ NSFontAttributeName : [UIFont fontWithName:@"HelveticaNeue" size:17.0] } context:nil];
CATextLayer *textLayer = [CATextLayer layer];
[textLayer setString:text];
[textLayer setForegroundColor:[UIColor redColor].CGColor];
[textLayer setFrame:labelRect];
[textLayer setFont:CFBridgingRetain([UIFont fontWithName:@"HelveticaNeue" size:17.0].fontName)];
[textLayer setAlignmentMode:kCAAlignmentCenter];
[textLayer setFontSize:17.0];
textLayer.masksToBounds = YES;
textLayer.position = CGPointMake(CGRectGetMidX(view.bounds), CGRectGetMidY(view.bounds));
[view.layer addSublayer:textLayer];

10

正则表达式和属性字符串的 Swift 3 版本。

class ECATextLayer: CATextLayer {
    override open func draw(in ctx: CGContext) {
        let yDiff: CGFloat
        let fontSize: CGFloat
        let height = self.bounds.height

        if let attributedString = self.string as? NSAttributedString {
            fontSize = attributedString.size().height
            yDiff = (height-fontSize)/2
        } else {
            fontSize = self.fontSize
            yDiff = (height-fontSize)/2 - fontSize/10
        }

        ctx.saveGState()
        ctx.translateBy(x: 0.0, y: yDiff)
        super.draw(in: ctx)
        ctx.restoreGState()
    }
}

完美!像魔法一样运行。 - Maschina
对于OSX,由于几何翻转,翻译需要为ctx.translateBy(x: 0.0, y: -yDiff) - Miro Markaravanes

6

感谢@iamktothed,它起作用了。以下是Swift 3版本:

class CXETextLayer : CATextLayer {

override init() {
    super.init()
}

override init(layer: Any) {
    super.init(layer: layer)
}

required init(coder aDecoder: NSCoder) {
    super.init(layer: aDecoder)
}

override func draw(in ctx: CGContext) {
    let height = self.bounds.size.height
    let fontSize = self.fontSize
    let yDiff = (height-fontSize)/2 - fontSize/10

    ctx.saveGState()
    ctx.translateBy(x: 0.0, y: yDiff)
    super.draw(in: ctx)
    ctx.restoreGState()
}
}

3

更新此线程(适用于单行和多行CATextLayer),结合以上一些答案。

class VerticalAlignedTextLayer : CATextLayer {

    func calculateMaxLines() -> Int {
        let maxSize = CGSize(width: frame.size.width, height: CGFloat(Float.infinity))
        let font = UIFont(descriptor: self.font!.fontDescriptor, size: self.fontSize)
        let charSize = font.lineHeight
        let text = (self.string ?? "") as! NSString
        let textSize = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
        let linesRoundedUp = Int(ceil(textSize.height/charSize))
        return linesRoundedUp
    }
    
    override func draw(in context: CGContext) {
        let height = self.bounds.size.height
        let fontSize = self.fontSize
        let lines = CGFloat(calculateMaxLines())
        let yDiff = (height - lines * fontSize) / 2 - lines * fontSize / 10

        context.saveGState()
        context.translateBy(x: 0, y: yDiff) // Use -yDiff when in non-flipped coordinates (like macOS's default)
        super.draw(in: context)
        context.restoreGState()
    }
}

我无法在Swift 5.3的macOS中使其工作。我用NSFont替换了UIFont,但XCode建议进行了一些其他更改,但它仍然无法正常工作。您是否有可能发布适用于Swift 5.3和macOS的版本? - SouthernYankee65
我使用Swift 5.0在macOS上制作了这个,我没有看到任何Xcode问题。您有关于哪些部分不起作用的详细信息吗? - nicksarno
将这个类粘贴到我的Swift文件中后,XCode立即告诉我“未解决的标识符'UIFont.'的使用”。如果我将其更改为'NSFont',则在下面的下一行中会出现“值类型'NSFont?'没有成员'lineHeight'。”。在两行以下有一个警告,读取“表达式从'NSFont?'隐式强制转换为'Any'”。 - SouthernYankee65
我已经解决了。我将其发布为 macOS 的解决方案。 - SouthernYankee65

3

没有什么能阻止您使用通用的CALayer(容器)创建带有CATextLayer子层的CALayer层次结构。

不需要为CATextLayer计算字体大小,只需计算CATextLayer在CALayer中的偏移量,使其垂直居中。如果将文本层的对齐模式设置为居中,并使文本层的宽度与封闭容器相同,则水平居中。

let container = CALayer()
let textLayer = CATextLayer()

// create the layer hierarchy
view.layer.addSublayer(container)
container.addSublayer(textLayer)

// Setup the frame for your container
...


// Calculate the offset of the text layer so that it is centred
let hOffset = (container.frame.size.height - textLayer.frame.size.height) * 0.5

textLayer.frame = CGRect(x:0.0, y: hOffset, width: ..., height: ...)

子层框架相对于其父层计算,因此计算相对简单。目前不需要考虑字体大小,这由处理CATextLayer的代码处理,而不是布局代码。

2

gbk的代码是可行的。下面是gbk的代码,更新为XCode 8 beta 6。截至2016年10月1日的最新版本。

步骤1. 子类化CATextLayer。在下面的代码中,我将子类命名为"MyCATextLayer"。在您的视图控制器类之外复制/粘贴以下代码。

class MyCATextLayer: CATextLayer {

// REF: http://lists.apple.com/archives/quartz-dev/2008/Aug/msg00016.html
// CREDIT: David Hoerl - https://github.com/dhoerl
// USAGE: To fix the vertical alignment issue that currently exists within the CATextLayer class. Change made to the yDiff calculation.

override init() {
    super.init()
}

required init(coder aDecoder: NSCoder) {
    super.init(layer: aDecoder)
}

override func draw(in ctx: CGContext) {
    let height = self.bounds.size.height
    let fontSize = self.fontSize
    let yDiff = (height-fontSize)/2 - fontSize/10

    ctx.saveGState()
    ctx.translateBy(x: 0.0, y: yDiff)
    super.draw(in: ctx)
    ctx.restoreGState()
   }
}

步骤2. 在您的“.swift”文件中的视图控制器类内,创建您的CATextLabel。在代码示例中,我将子类命名为“MyDopeCATextLayer”。

let MyDopeCATextLayer: MyCATextLayer = MyCATextLayer()

第三步:设置您的新CATextLayer,包括所需的文本/颜色/边界/框架。
MyDopeCATextLayer.string = "Hello World"    // displayed text
MyDopeCATextLayer.foregroundColor = UIColor.purple.cgColor //color of text is purple
MyDopeCATextLayer.frame = CGRect(x: 0, y:0, width: self.frame.width, height: self.frame.height)  
MyDopeCATextLayer.font = UIFont(name: "HelveticaNeue-UltraLight", size: 5) //5 is ignored, set actual font size using  ".fontSize" (below)
MyDopeCATextLayer.fontSize = 24
MyDopeCATextLayer.alignmentMode = kCAAlignmentCenter //Horizontally centers text.  text is automatically centered vertically because it's set in subclass code
MyDopeCATextLayer.contentsScale = UIScreen.main.scale  //sets "resolution" to whatever the device is using (prevents fuzzyness/blurryness)

第四步完成。


1
class CenterTextLayer: CATextLayer {

override func draw(in ctx: CGContext) {
    #if os(iOS) || os(tvOS)
    let multiplier = CGFloat(1)
    #elseif os(OSX)
    let multiplier = CGFloat(-1)
    #endif
    let yDiff = (bounds.size.height - ((string as? NSAttributedString)?.size().height ?? fontSize)) / 2 * multiplier
    ctx.saveGState()
    ctx.translateBy(x: 0.0, y: yDiff)
    super.draw(in: ctx)
    ctx.restoreGState()
}

}

致谢:

https://github.com/cemolcay/CenterTextLayer


1
我稍微修改了 @iamkothed的这篇答案。它们之间的不同之处在于:
  • text height calculation is based on NSString.size(with: Attributes). I don't know if it's an improvement over (height-fontSize)/2 - fontSize/10, but I like to think that it is. Although, in my experience, NSString.size(with: Attributes) doesn't always return the most appropriate size.
  • added invertedYAxis property. It was useful for my purposes of exporting this CATextLayer subclass using AVVideoCompositionCoreAnimationTool. AVFoundation operates in "normal" y axis, and that's why I had to add this property.
  • Works only with NSString. You can use Swift's String class though, because it automatically casts to NSString.
  • It ignores CATextLayer.fontSize property and completely relies on CATextLayer.font property which MUST be a UIFont instance.

    class VerticallyCenteredTextLayer: CATextLayer {
        var invertedYAxis: Bool = true
    
        override func draw(in ctx: CGContext) {
            guard let text = string as? NSString, let font = self.font as? UIFont else {
                super.draw(in: ctx)
                return
            }
    
            let attributes = [NSAttributedString.Key.font: font]
            let textSize = text.size(withAttributes: attributes)
            var yDiff = (bounds.height - textSize.height) / 2
            if !invertedYAxis {
                yDiff = -yDiff
            }
            ctx.saveGState()
            ctx.translateBy(x: 0.0, y: yDiff)
            super.draw(in: ctx)
            ctx.restoreGState()
        }
    }
    

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接