如何让UILabel显示描边文字?

120

我想要的只是白色UILabel文本周围一像素的黑色边框。

我已经通过以下代码创建了UILabel子类,这些代码是从一些有关示例中拼凑而成的。它能够工作,但速度非常慢(除了模拟器之外),我也无法使文本在垂直方向上居中(所以我暂时将y值硬编码到了最后一行)。啊啊啊!

void ShowStringCentered(CGContextRef gc, float x, float y, const char *str) {
    CGContextSetTextDrawingMode(gc, kCGTextInvisible);
    CGContextShowTextAtPoint(gc, 0, 0, str, strlen(str));
    CGPoint pt = CGContextGetTextPosition(gc);

    CGContextSetTextDrawingMode(gc, kCGTextFillStroke);

    CGContextShowTextAtPoint(gc, x - pt.x / 2, y, str, strlen(str));
}


- (void)drawRect:(CGRect)rect{

    CGContextRef theContext = UIGraphicsGetCurrentContext();
    CGRect viewBounds = self.bounds;

    CGContextTranslateCTM(theContext, 0, viewBounds.size.height);
    CGContextScaleCTM(theContext, 1, -1);

    CGContextSelectFont (theContext, "Helvetica", viewBounds.size.height,  kCGEncodingMacRoman);

    CGContextSetRGBFillColor (theContext, 1, 1, 1, 1);
    CGContextSetRGBStrokeColor (theContext, 0, 0, 0, 1);
    CGContextSetLineWidth(theContext, 1.0);

    ShowStringCentered(theContext, rect.size.width / 2.0, 12, [[self text] cStringUsingEncoding:NSASCIIStringEncoding]);
}

我只是有一种困扰的感觉,觉得我可能忽视了一个更简单的方法来做这件事。也许可以通过覆盖“drawTextInRect”来实现,但是尽管我一直盯着它看并且非常用力地皱眉,但似乎无法让“drawTextInRect”顺从于我。


澄清一下 - 我的应用程序变慢是因为我在标签值更改时使用了轻微的增长和缩小动画。如果不使用子类化,它会很流畅,但是使用上面的代码后,标签动画就会非常卡顿。我应该只使用UIWebView吗?我觉得这样做有点傻,因为标签只显示一个数字... - Monte Hurd
好的,看起来我之前遇到的性能问题与大纲代码无关,但我仍然无法垂直对齐。不知何故,pt.y 始终为零。 - Monte Hurd
对于像Chalkduster这样的字体来说,这非常慢。 - jjxtra
15个回答

167

我能够通过重写drawTextInRect:方法来实现它。

- (void)drawTextInRect:(CGRect)rect {

  CGSize shadowOffset = self.shadowOffset;
  UIColor *textColor = self.textColor;

  CGContextRef c = UIGraphicsGetCurrentContext();
  CGContextSetLineWidth(c, 1);
  CGContextSetLineJoin(c, kCGLineJoinRound);

  CGContextSetTextDrawingMode(c, kCGTextStroke);
  self.textColor = [UIColor whiteColor];
  [super drawTextInRect:rect];

  CGContextSetTextDrawingMode(c, kCGTextFill);
  self.textColor = textColor;
  self.shadowOffset = CGSizeMake(0, 0);
  [super drawTextInRect:rect];

  self.shadowOffset = shadowOffset;

}

3
我尝试了这个技巧,但结果不是很令人满意。在我的iPhone 4上,我尝试使用这段代码来绘制带有黑色轮廓的白色文本。我得到的是每个字母左右边缘的黑色轮廓,但顶部和底部没有轮廓。有什么想法吗? - Mike Hershberg
1
@Momeks:如果你不知道如何在Objective-C中创建子类,那么你应该去Google一下;苹果有一份Objective-C语言指南。 - Jeff Kelley
1
可能吧 - 我觉得在我写这篇文章的2.5年前那个还不存在 :) - kprevas
如果你“交换”绘制填充和描边,你会得到更好的结果。否则描边可能会有点偏差... - Stefan
对于像Chalkduster这样的字体来说,这是非常慢的。 - jjxtra
显示剩余10条评论

129

使用 Attributed String 是一种更简单的解决方案,示例如下:

Swift 4:

let strokeTextAttributes: [NSAttributedStringKey : Any] = [
    NSAttributedStringKey.strokeColor : UIColor.black,
    NSAttributedStringKey.foregroundColor : UIColor.white,
    NSAttributedStringKey.strokeWidth : -2.0,
    ]

myLabel.attributedText = NSAttributedString(string: "Foo", attributes: strokeTextAttributes)

Swift 4.2:

let strokeTextAttributes: [NSAttributedString.Key : Any] = [
    .strokeColor : UIColor.black,
    .foregroundColor : UIColor.white,
    .strokeWidth : -2.0,
    ]

myLabel.attributedText = NSAttributedString(string: "Foo", attributes: strokeTextAttributes)

UITextField中,您可以设置defaultTextAttributesattributedPlaceholder

请注意,在这种情况下,NSStrokeWidthAttributeName必须为负数,即只有内部轮廓有效。

使用属性文本的UITextFields带有轮廓


18
为了方便在Objective-C中,可以这样写: label.attributedText = [[NSAttributedString alloc] initWithString:@"字符串" attributes:@{ NSStrokeColorAttributeName : [UIColor blackColor], NSForegroundColorAttributeName : [UIColor whiteColor], NSStrokeWidthAttributeName : @-1.0 }]; - Leszek Szary
12
这个meme的使用有效地传达了这种方法的细微差别。 - woody121
Swift 4.2: let strokeTextAttributes: [String : Any] = [ NSStrokeColorAttributeName : UIColor.black, NSForegroundColorAttributeName : UIColor.white, NSStrokeWidthAttributeName : -4.0, ] consoleView.typingAttributes = strokeTextAttributes - Teo Sartori
谢谢@TeoSartori,我在我的回答中添加了Swift 4.2语法;) - Axel Guilmin

28

在阅读了被接受的答案、两个更正和 Axel Guilmin 的答案后,我决定在 Swift 中编写一种适合我的综合解决方案:

import UIKit

class UIOutlinedLabel: UILabel {

    var outlineWidth: CGFloat = 1
    var outlineColor: UIColor = UIColor.whiteColor()

    override func drawTextInRect(rect: CGRect) {

        let strokeTextAttributes = [
            NSStrokeColorAttributeName : outlineColor,
            NSStrokeWidthAttributeName : -1 * outlineWidth,
        ]

        self.attributedText = NSAttributedString(string: self.text ?? "", attributes: strokeTextAttributes)
        super.drawTextInRect(rect)
    }
}

您可以将此自定义UILabel类添加到Interface Builder中现有的标签中,并通过添加用户定义的运行时属性来更改边框的厚度和颜色,如下所示:

Interface Builder setup

结果:

big red G with outline


1
好的,我会用这个创建一个IBInspectable :) - Mário Carvalho
@Lachezar,如何使用UIButton的文本完成此操作? - CrazyOne

10

答案实现中存在一个问题。描边文本的字符字形宽度与不描边文本略有不同,可能会产生“非居中”结果。可以通过在填充文本周围添加一个不可见的描边来解决这个问题。

替换为:

CGContextSetTextDrawingMode(c, kCGTextFill);
self.textColor = textColor;
self.shadowOffset = CGSizeMake(0, 0);
[super drawTextInRect:rect];

与:

CGContextSetTextDrawingMode(context, kCGTextFillStroke);
self.textColor = textColor;
[[UIColor clearColor] setStroke]; // invisible stroke
self.shadowOffset = CGSizeMake(0, 0);
[super drawTextInRect:rect];

我不完全确定这是否是真的交易,因为我不知道self.textColor = textColor;是否与[textColor setFill]具有相同的效果,但它应该有效。

声明:我是THLabel的开发人员。

一段时间以前,我发布了一个UILabel的子类,可以在文本中添加轮廓和其他效果。你可以在这里找到它:https://github.com/tobihagemann/THLabel


4
我刚刚使用了THLabel,生活已经够复杂了。 - Prat
1
多亏了THLabel,我只需添加两行代码就能在文本周围绘制轮廓。感谢提供解决方案,让我不再花费太长时间来解决这个问题! - Alan Kinnaman
我很惊讶没有人注意到,但内置的文本描边命令并不创建轮廓,而是创建了一个“内联” - 即描边绘制在字母的内部,而不是外部。使用THLabel,我们可以选择将描边实际绘制在外部。做得好! - lenooh

6

这样做不会真正创建一个轮廓,但它会在文本周围产生阴影,如果您将阴影半径调得足够小,它就可以类似于轮廓。

label.layer.shadowColor = [[UIColor blackColor] CGColor];
label.layer.shadowOffset = CGSizeMake(0.0f, 0.0f);
label.layer.shadowOpacity = 1.0f;
label.layer.shadowRadius = 1.0f;

我不知道它是否与旧版本的iOS兼容。

无论如何,我希望它能有所帮助...


9
这不是标签周围的一个像素边框,而是简单的向下投射的阴影。 - Tim
1
这是错误的解决方案。天哪,谁标记了它?! - Sam B
他们很清楚地说“轮廓”,这是一个阴影。并没有回答问题。 - hitme
嗯,如果这是目标(比如白色背景上的白色文字),它会产生非常好的轮廓效果。如果你想要一个“真正”的轮廓,请查看其他答案。附注:也适用于i.Ex. UIButtons。拯救了我的用户界面。很酷的方法! - undefined

6

基于kprevas的答案,这是一个Swift 4类版本。

import Foundation
import UIKit

public class OutlinedText: UILabel{
    internal var mOutlineColor:UIColor?
    internal var mOutlineWidth:CGFloat?

    @IBInspectable var outlineColor: UIColor{
        get { return mOutlineColor ?? UIColor.clear }
        set { mOutlineColor = newValue }
    }

    @IBInspectable var outlineWidth: CGFloat{
        get { return mOutlineWidth ?? 0 }
        set { mOutlineWidth = newValue }
    }    

    override public func drawText(in rect: CGRect) {
        let shadowOffset = self.shadowOffset
        let textColor = self.textColor

        let c = UIGraphicsGetCurrentContext()
        c?.setLineWidth(outlineWidth)
        c?.setLineJoin(.round)
        c?.setTextDrawingMode(.stroke)
        self.textColor = mOutlineColor;
        super.drawText(in:rect)

        c?.setTextDrawingMode(.fill)
        self.textColor = textColor
        self.shadowOffset = CGSize(width: 0, height: 0)
        super.drawText(in:rect)

        self.shadowOffset = shadowOffset
    }
}

您可以通过将UILabel的自定义类设置为OutlinedText,在界面构建器中完全实现它。然后,您就能够从属性窗格中设置轮廓线的宽度和颜色。 enter image description here

也非常适用于Swift 5。 - Farkas Antal
文本在两侧被裁剪。 - dub
这个标签在内存中占用了5MB的空间 - 真疼! - timbre timbre
对我没有用。什么都没发生。 - submariner

5

如果您的目标是这样的:

enter image description here

以下是我如何实现它的方法:我向我的当前UILabel添加了一个自定义类别的新

只需将其复制粘贴到您的项目中即可使用:

extension UILabel {
    func addTextOutline(usingColor outlineColor: UIColor, outlineWidth: CGFloat) {
        class OutlinedText: UILabel{
            var outlineWidth: CGFloat = 0
            var outlineColor: UIColor = .clear

            override public func drawText(in rect: CGRect) {
                let shadowOffset = self.shadowOffset
                let textColor = self.textColor

                let c = UIGraphicsGetCurrentContext()
                c?.setLineWidth(outlineWidth)
                c?.setLineJoin(.round)
                c?.setTextDrawingMode(.stroke)
                self.textAlignment = .center
                self.textColor = outlineColor
                super.drawText(in:rect)

                c?.setTextDrawingMode(.fill)
                self.textColor = textColor
                self.shadowOffset = CGSize(width: 0, height: 0)
                super.drawText(in:rect)

                self.shadowOffset = shadowOffset
            }
        }

        let textOutline = OutlinedText()
        let outlineTag = 9999

        if let prevTextOutline = viewWithTag(outlineTag) {
            prevTextOutline.removeFromSuperview()
        }

        textOutline.outlineColor = outlineColor
        textOutline.outlineWidth = outlineWidth
        textOutline.textColor = textColor
        textOutline.font = font
        textOutline.text = text
        textOutline.tag = outlineTag

        sizeToFit()
        addSubview(textOutline)
        textOutline.frame = CGRect(x: -(outlineWidth / 2), y: -(outlineWidth / 2),
                                   width: bounds.width + outlineWidth,
                                   height: bounds.height + outlineWidth)
    }
}

使用方法:

yourLabel.addTextOutline(usingColor: .red, outlineWidth: 6)

这也适用于 UIButton 及其所有动画效果:

yourButton.titleLabel?.addTextOutline(usingColor: .red, outlineWidth: 6)

此选项原样使用会破坏多行。 - Gonçalo Gaspar
2
这个答案比其他一些更好,因为它不是在字符内部绘制,而是在周围绘制。对于其他一些答案,如果你增加宽度到足够大,就没有字符了,只有轮廓。 - Victor Engel
我将文本居中对齐,但轮廓文本仍然是左对齐的。我该如何修复? - submariner

4
如果您想制作复杂的动画,最好的方法是编程地截取其屏幕截图并对其进行动画处理!
要截取视图的屏幕截图,您需要编写类似以下代码的代码:
UIGraphicsBeginImageContext(mainContentView.bounds.size);
[mainContentView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext(); 

其中mainContentView是您想要截屏的视图。将viewImage添加到UIImageView中并对其进行动画处理。

希望这能加快您的动画速度!

N


这是一个很棒的技巧,我可以想到几个地方可以使用它,并且可能解决了性能问题,但我仍然希望有比我的糟糕子类更简单的方法来显示uilabel文本的轮廓。与此同时,我会尝试你的示例,谢谢! - Monte Hurd
我能理解你的痛苦。对于这件事,我不确定你能找到一个好的解决办法。我经常写很多代码来生成效果,然后将它们快照下来(就像上面那样),以实现平滑的动画!对于上面的示例,你需要在代码中包含<QuartzCore/QuartzCore.h>!祝你好运!N - Nick Cartwright

4
MuscleRumble所提到的,被接受的答案的边框有些偏离中心。我通过将描边宽度设置为零而不是更改颜色为透明来纠正了这个问题。
即用以下内容替换:
CGContextSetTextDrawingMode(c, kCGTextFill);
self.textColor = textColor;
self.shadowOffset = CGSizeMake(0, 0);
[super drawTextInRect:rect];

使用:

CGContextSetTextDrawingMode(c, kCGTextFillStroke);
self.textColor = textColor;
CGContextSetLineWidth(c, 0); // set stroke width to zero
self.shadowOffset = CGSizeMake(0, 0);
[super drawTextInRect:rect];

我本想在他的回答下发表评论,但显然我的“声望”不够。


2

如果你只想在我的白色UILabel文本周围加一个一像素的黑色边框,那么我认为你正在把问题复杂化了...

我不记得应该使用哪个“draw rect / frameRect”函数,但你很容易找到。这种方法只是演示了策略(让超类完成工作!):

- (void)drawRect:(CGRect)rect
{
  [super drawRect:rect];
  [context frameRect:rect]; // research which rect drawing function to use...
}

我不明白。可能是因为我已经盯着屏幕看了大约12个小时,现在已经无法理解太多了... - Monte Hurd
你正在对UILabel进行子类化,这个类已经可以绘制文本。而你之所以要进行子类化,是因为你想在文本周围添加一个黑色边框。因此,如果让超类绘制文本,那么你只需要绘制边框就可以了,是吗? - kent
这是一个很好的观点...虽然,如果他想添加一个1像素的黑色边框,子类可能会把字母画得太靠近了!...我会做一些像原始问题中发布的东西,并进行优化(如我在帖子中所述)! - Nick Cartwright
@nickcartwright:如果发生了你所说的情况,即超类正在执行操作,你可以在调用[super drawRect]之前插入CGRect。这样比重新编写超类的功能更简单、更安全。 - kent
然后@kent因为沮丧而离开了SO。好答案,加一。 - Dan Rosenstark

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接