SpriteKit: Is there a way to centre a SKLabelNode

2019-03-02 17:15发布

A label has four different verticalAlignmentMode: .Baseline, .Bottom, .Center and .Top.

I would like the label to be centred on its position according to its baseline. .Center doesn't work for me because the bottom of the frame isn't the baseline of the text, but rather, the bottom of the lowest letter (like a 'y', for example).

I have also tried using .Baseline and subtracting half the frame's height from the y-position, but this doesn't work either and results in the same problem as .Center.

The text in the label I'm trying to centre is "Play!". Setting the mode to '.Center' makes the text slightly higher than what I want it to be which is quite noticeable. Changing the text to "Pla!" fixes the problem because the 'y' is removed and none of the characters hang below the baseline (I obviously can't do this as the solution, though).

I would like a suggestion for a way around this problem — perhaps there is some way to get the position of the baseline? Thanks!

1条回答
姐就是有狂的资本
2楼-- · 2019-03-02 18:05

So I've had the same issue and with the help with of this question and answer I found a decent solution.

I wanted to align the text of label nodes with custom button nodes (a label node wrapped in a shape node to draw the outline).

I also wanted to align toggle (on/off) buttons with the baseline of the label node and use the same height as the xHeight of the text in the label.

Aligning labels, buttons and shapes The red lines show that all labels & buttons are properly aligned.

So in order to do this, I've done the following.

I created a Font class that can be used to create a font object from the fontName and fontSize in a SKLabelNode.

Internally it will create either a NSFont (macOS) or UIFont (iOS). And set the ascender, descender properties. For convenience there's also a getter that returns the maximum possible height of the font.

import Foundation

#if os(macOS)
import Cocoa
#endif

#if os(iOS)
import UIKit
#endif

enum FontError: LocalizedError {
    case invalidFont(String, CGFloat)

    var errorDescription: String? {
        switch self {
        case .invalidFont(let name, let size):
            return "Could not create font with name \(name) and size: \(size)"
        }
    }
}

class Font {
    private(set) var name: String
    private(set) var size: CGFloat

    private(set) var ascender: CGFloat
    private(set) var descender: CGFloat
    private(set) var xHeight: CGFloat

    var maxHeight: CGFloat {
        return self.ascender + fabs(self.descender)
    }

    init(name: String, size: CGFloat) throws {
        // check if font can be created, otherwise crash
        // set ascender, descender, etc...
        #if os(macOS)
        guard let font = NSFont(name: name, size: size) else {
            throw FontError.invalidFont(name, size)
        }

        self.ascender = font.ascender
        self.descender = font.descender
        self.xHeight = font.xHeight
        #endif

        #if os(iOS)
        guard let font = UIFont(name: name, size: size) else {
            throw FontError.invalidFont(name, size)
        }
        self.ascender = font.ascender
        self.descender = font.descender
        self.xHeight = font.xHeight
        #endif

        self.name = name
        self.size = size
    }
}

I then use the maxHeight in my label position calculations. So for example in my custom ButtonNode (a SKShapeNode that contains a SKLabelNode) I do the following (simplified):

// in the constructor of my custom 
// button node ...
let height = 30

self.label = SKLabelNode(text: "Back")
// in order to position properly, set 
// vertical alignment to baseline
self.label.verticalAlignmentMode = .baseline

let font = try Font(name: self.label.fontName!, size: self.label.fontSize)
// depending on the font you might want to add 
// an additional y offset to place labels 
// higher or lower within the node
let yOffset = (height - font.maxHeight) / 2)
let labelWidth = self.label.calculateAccumulatedFrame().width

let labelFrame = CGRect(
    x: 0,
    y: 0,
    width: labelWidth,
    height: height
)

self.path = CGPath(roundedRect: labelFrame, cornerWidth: 5, cornerHeight: 5, transform: nil)

self.label.position = CGPoint(x: labelFrame.width / 2, y: yOffset)

P.S.: If you prefer to not create a separate Font class and you still want the code to work properly on iOS and macOS, you could just create an typealias for NSFont and UIFont. Since you can use the same constructor, you'd only need to add an extension to calculate the max height of the font (ascender + abs(descender)).

I prefer to create a class so I can throw an error if the font could not be created properly.

查看更多
登录 后发表回答