首页 > 解决方案 > 单行 UILabel - 基线位置错误

问题描述

我在自定义 UILabel 子类中有错误的垂直基线位置numberOfLines = 1(红线显示基线):

在此处输入图像描述

但在numberOfLines = 0或其他正值的情况下它工作正常:

在此处输入图像描述

此自定义标签具有:

在覆盖的方法中,我补偿了文本剪辑,这是因为行高值用于计算标签渲染矩形,并且对于此目的来说太小了。这是代码:

class CustomLabel: UILabel
{
    var lineHeight: CGFloat!
    
    override func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect
    {
        func snapToScreenScale(_ length: CGFloat) -> CGFloat
        {
            let scale = UIScreen.main.scale
            return ceil(length * scale) / scale
        }
        
        func compensation() -> CGFloat
        {
            // text inside line area is attached to bottom and may be clipped at top
            // so to compensate clipping we should add difference between required and original line heights
            let topCompensation = max(
                0,
                snapToScreenScale(
                    font.lineHeight - lineHeight
                )
            )
            
            // lines of text are centered verticaly in label's bounds
            // so to compensate top clipping completely we should add equal extra space at top and bottom
            let compensation = topCompensation * 2
            
            return compensation
        }
        
        var rect = super.textRect(
            forBounds: bounds,
            limitedToNumberOfLines: numberOfLines
        )
        rect.size = .init(
            width: snapToScreenScale(rect.width),
            height: snapToScreenScale(rect.height)
        )
        rect.size.height += compensation()
        
        return rect
    }
}

以及如何使用标签:

let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.minimumLineHeight = 32
paragraphStyle.maximumLineHeight = 32

let label = CustomLabel()
label.backgroundColor = .lightGray
label.lineHeight = 32
label.numberOfLines = 1
label.attributedText = NSAttributedString(
    string: "Text",
    attributes: [
        .font: UIFont.systemFont(ofSize: 32),
        .paragraphStyle: paragraphStyle
    ]
)
view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.topAnchor.constraint(equalTo: view.topAnchor, constant: 16).isActive = true
label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16).isActive = true
label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16).isActive = true

let baseline = UIView()
baseline.backgroundColor = .red.withAlphaComponent(0.2)
view.addSubview(baseline)
baseline.translatesAutoresizingMaskIntoConstraints = false
baseline.topAnchor.constraint(equalTo: label.firstBaselineAnchor).isActive = true
baseline.leadingAnchor.constraint(equalTo: label.leadingAnchor).isActive = true
baseline.trailingAnchor.constraint(equalTo: label.trailingAnchor).isActive = true
baseline.heightAnchor.constraint(equalToConstant: 1).isActive = true

注意,由于项目要求:

有趣的细节:调用superintextRect(forBounds:limitedToNumberOfLines:)有副作用;如果未调用,则基线将附加到标签的顶部:

在此处输入图像描述

因此,标签的行为显然是错误的。尝试通过调用标签的方法来修复约束

setNeedsUpdateConstraints()
updateConstraintsIfNeeded()

没有帮助,在不同的时刻尝试(标签初始化后,矩形计算后,布局后)。

在 iOS 12.0 - 14.5 中测试。

将欣赏任何想法。

标签: iosuilabel

解决方案


添加了解决方法。想法是设置numberOfLines为0,因为那时不会发生错误;为了限制行数,我用所需的行数计算文本 rect,因此额外的文本不适合 rect 并被换行。

代码说明了这种方法;请注意,此代码只是一个概念,并不可靠或设计良好等。

class CustomLabel: UILabel
{
    var lineHeight: CGFloat!
    var requiredNumberOfLines: Int!

    override var numberOfLines: Int
    {
        get
        {
            return requiredNumberOfLines
        }
        set
        {
            requiredNumberOfLines = newValue
            super.numberOfLines = 0 // override to prevent bug with incorrect baseline position
        }
    }
    
    override func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect
    {
        func snapToScreenScale(_ length: CGFloat) -> CGFloat
        {
            let scale = UIScreen.main.scale
            return ceil(length * scale) / scale
        }
        
        func compensation() -> CGFloat
        {
            // text inside line area is attached to bottom and may be clipped at top
            // so to compensate clipping we should add difference between required and original line heights
            let topCompensation = max(
                0,
                snapToScreenScale(
                    font.lineHeight - lineHeight
                )
            )
            
            // lines of text are centered verticaly in label's bounds
            // so to compensate top clipping completely we should add equal extra space at top and bottom
            let compensation = topCompensation * 2
            
            return compensation
        }
        
        var rect = super.textRect(
            forBounds: bounds,
            limitedToNumberOfLines: numberOfLines
        )
        rect.size = .init(
            width: snapToScreenScale(rect.width),
            height: snapToScreenScale(rect.height)
        )
        rect.size.height += compensation()
        
        return rect
    }
}

推荐阅读