I am working on a CATextLayer that I want to use in both Mac and iOS. Can I control the vertical alignment of the text within the layer?
In this particular case, I want to center it vertically -- but information about other vertical alignments would also be of interest.
EDIT: I found this, but I can't make it work.
1 Select the text you want to center between the top and bottom margins. 2 On the Page Layout tab, click the Page Setup Dialog Box Launcher. 3 Select the Layout tab. 4 In the Vertical alignment box, click Center 5 In the Apply to box, click Selected text, and then click OK.
To center both vertically and horizontally, use padding and text-align: center : I am vertically and horizontally centered.
If you have a multiline text, you can divide it in <span> tags and adjust your CSS to display them as block elements. Then, adjust line-height and padding-top of the <span> tags in the CSS to align them vertically.
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()
}
}
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.
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
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
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
If all characters would be occurred in your text, then y equals to:
y = font.leading / 2
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];
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()
}
}
There is nothing stopping you from creating a CALayer hierarchy with a generic CALayer (container) that has the CATextLayer as a sublayer.
Instead of calculating font sizes for the CATextLayer, simply calculate the offset of the CATextLayer inside the CALayer so that it is vertically centred. If you set the alignment mode of the text layer to centred and make the width of the text layer the same as the enclosing container it also centres horizontally.
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: ...)
The sublayer frame is relative to its parent, so the calculation is fairly straightforward. No need to care at this point about font sizes. That's handled by your code dealing with the CATextLayer, not in the layout code.
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
Updating this thread (for single and multi line CATextLayer), combining some answers above.
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()
}
}
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()
}
}
}
I slightly modified this answer by @iamkothed. The differences are:
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.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.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()
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With