Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

vertically align text in a CATextLayer?

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.

like image 961
William Jockusch Avatar asked Jan 22 '11 01:01

William Jockusch


People also ask

How do you align text vertically?

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.

How do I align text vertically in CSS?

To center both vertically and horizontally, use padding and text-align: center : I am vertically and horizontally centered.

How do you vertically align text in block element?

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.


10 Answers

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()
    }
}
like image 148
iamktothed Avatar answered Sep 22 '22 16:09

iamktothed


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:

fig

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
    
like image 38
Enix Avatar answered Sep 22 '22 16:09

Enix


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];
like image 44
hbk Avatar answered Sep 21 '22 16:09

hbk


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()
    }
}
like image 30
Hank Frinkle Avatar answered Sep 25 '22 16:09

Hank Frinkle


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()
}
}
like image 31
leon wu Avatar answered Sep 21 '22 16:09

leon wu


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.

like image 39
obj-D Avatar answered Sep 22 '22 16:09

obj-D


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

like image 22
vapor Avatar answered Sep 22 '22 16:09

vapor


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()
    }
}
like image 45
nicksarno Avatar answered Sep 24 '22 16:09

nicksarno


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()
        }
    }
}
like image 35
AlexSmet Avatar answered Sep 25 '22 16:09

AlexSmet


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()
        }
    }
    
like image 42
xinatanil Avatar answered Sep 23 '22 16:09

xinatanil