vertically align text in a CATextLayer?

允我心安 提交于 2019-11-29 05:30:34
iamktothed

The correct answer, as you've already found, is here in Objective-C and works for iOS. It works by subclassing the CATextLayer and overriding the drawInContext function.

However, I've made some improvements to the code, as shown below, using David Hoerl's code as a basis. The changes come solely in recalculating the vertical position of the text represented by the yDiff. I've tested it with my own code.

Here is the code for Swift users:

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()     } } 
gbk

Maybe to late for answer, but you can calculate size of text and then set position of textLayer. Also you need to put textLayer textAligment mode to "center"

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]; 

It is an late answer, but I have the same question these days, and have solved the problem with following investigation.

Vertical align depends on the text you need to draw, and the font you are using, so there is no one way solution to make it vertical for all cases.

But we can still calculate the vertical mid point for different cases.

According to apple's About Text Handling in iOS, we need to know how the text is drawn.

For example, I am trying to make vertical align for weekdays strings: Sun, Mon, Tue, ....

For this case, the height of the text depends on cap Height, and there is no descent for these characters. So if we need to make these text align to the middle, we can calculate the offset of the top of cap character, e.g. The position of the top of character "S".

According to the the figure below:

The top space for the capital character "S" would be

font.ascender - font.capHeight 

And the bottom space for the capital character "S" would be

font.descender + font.leading 

So we need to move "S" a little bit off the top by:

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

That equals to:

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

Then I can make the text vertical align middle.

Conclusion:

  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 

Swift 3 version for regular and attributed strings.

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

thank @iamktothed, it works. following is swift 3 version:

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

gbk's code works. below is gbk's code updated for XCode 8 beta 6. Current as of 1 Oct 2016

Step 1. Subclass CATextLayer. In the code below I've named the subclass "MyCATextLayer" Outside your view controller class copy/paste the below code.

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

Step 2. Within your view controller class in your ".swift" file, create your CATextLabel. In the code example I've named the subclass "MyDopeCATextLayer."

let MyDopeCATextLayer: MyCATextLayer = MyCATextLayer() 

Step 3. Set your new CATextLayer with desired text/color/bounds/frame.

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) 

Step 4. done

The code for Swift 3, based on code @iamktothed

If you use an attributed string for setting font properties, than you can use function size() from NSAttributedString to calculate height of string. I think this code also resolve the problems described by @Enix

class LCTextLayer: CATextLayer {      override init() {         super.init()     }      override init(layer: Any) {         super.init(layer: layer)     }      required init(coder aDecoder: NSCoder) {         super.init(layer: aDecoder)     }      override open func draw(in ctx: CGContext) {          if let attributedString = self.string as? NSAttributedString {              let height = self.bounds.size.height             let stringSize = attributedString.size()             let yDiff = (height - stringSize.height) / 2              ctx.saveGState()             ctx.translateBy(x: 0.0, y: yDiff)             super.draw(in: ctx)             ctx.restoreGState()         }     } } 

So there is no "direct" way of doing this but you can accomplish the same thing by using text metrics:

http://developer.apple.com/library/ios/#documentation/UIKit/Reference/NSString_UIKit_Additions/Reference/Reference.html

... for example, find the size of the text then use that information to place it where you want in the parent layer. Hope this helps.

t0rst

You need to know where CATextLayer will put the baseline of your text. Once you know that, offset the coordinate system within the layer, i.e. adjust bounds.origin.y by the difference between where the baseline normally sits and where you want it to be, given the metrics of the font.

CATextLayer is a bit of a black box and finding where the baseline will sit is a bit tricky - see my answer here for iOS - I've no idea what the behaviour is on Mac.

I slightly modified this answer by @iamkothed. The differences are:

  • 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()     } } 

I'd like to propose a solution that takes multiline wrapping inside the available box into account:

final class CACenteredTextLayer: CATextLayer {     override func draw(in ctx: CGContext) {         guard let attributedString = string as? NSAttributedString else { return }          let height = self.bounds.size.height         let boundingRect: CGRect = attributedString.boundingRect(             with: CGSize(width: bounds.width,                          height: CGFloat.greatestFiniteMagnitude),             options: NSStringDrawingOptions.usesLineFragmentOrigin,             context: nil)         let yDiff: CGFloat = (height - boundingRect.size.height) / 2          ctx.saveGState()         ctx.translateBy(x: 0.0, y: yDiff)         super.draw(in: ctx)         ctx.restoreGState()     } } 

As best I can tell, the answer to my question is "No."

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!