tl;dr: 你可以点击测试文本的路径。 Gist is available here.
我会采用的方法是检查点击点是否在文本路径内。在详细介绍之前,让我先概述一下这些步骤。
- 子类 UILabel
- 使用Core Text获取文本的CGPath
- 覆盖
pointInside:withEvent: 以能够确定是否应将点视为内部。
- 使用任何“正常”触摸处理(例如点击手势识别器)来了解点击的时间。
这种方法的最大优点是它可以精确地跟随字体,并且您可以修改路径以扩大“可点击”区域,如下所示。黑色和橙色部分都是可点击的,但标签中只会绘制黑色部分。
子类 UILabel
我创建了一个名为 TextHitTestingLabel 的 UILabel 的子类,并为文本路径添加了一个私有属性。
@interface TextHitTestingLabel (/*Private stuff*/)
@property (assign) CGPathRef textPath;
@end
由于 iOS 标签可以有 text 或 attributedText,所以我将这两个方法子类化并让它们调用一个方法来更新文本路径。
- (void)setText:(NSString *)text {
[super setText:text];
[self textChanged];
}
- (void)setAttributedText:(NSAttributedString *)attributedText {
[super setAttributedText:attributedText];
[self textChanged];
}
此外,可以从 NIB/Storyboard 创建标签,在这种情况下,文本将立即设置。在这种情况下,我会在 nib 中检查初始文本。
- (void)awakeFromNib {
[self textChanged];
}
使用Core Text获取文本的路径
Core Text 是一个低级框架,可让您完全控制文本呈现。您必须将CoreText.framework 添加到您的项目中并将其导入到您的文件中
#import <CoreText/CoreText.h>
我在textChanged 中做的第一件事是获取文本。根据它是 iOS 6 或更早版本,我还必须检查属性文本。一个标签将只有其中一个。
// Get the text
NSAttributedString *attributedString = nil;
if ([self respondsToSelector:@selector(attributedText)]) { // Available in iOS 6
attributedString = self.attributedText;
}
if (!attributedString) { // Either earlier than iOS6 or the `text` property was set instead of `attributedText`
attributedString = [[NSAttributedString alloc] initWithString:self.text
attributes:@{NSFontAttributeName: self.font}];
}
接下来,我为所有字母字形创建一个新的可变路径。
// Create a mutable path for the paths of all the letters.
CGMutablePathRef letters = CGPathCreateMutable();
核心文本“魔法”
Core Text 适用于文本行、字形和字形运行。例如,如果我有文本:“Hello”,其属性类似于“Hel lo”(为清楚起见添加了空格)。那么这将是一行文本,带有两个字形运行:一个粗体和一个常规。第一个字形运行包含 3 个字形,第二个运行包含 2 个字形。
我列举了所有字形运行及其字形并使用CTFontCreatePathForGlyph() 获取路径。然后将每个单独的字形路径添加到可变路径中。
// Create a line from the attributed string and get glyph runs from that line
CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef)attributedString);
CFArrayRef runArray = CTLineGetGlyphRuns(line);
// A line with more then one font, style, size etc will have multiple fonts.
// "Hello" formatted as " *Hel* lo " (spaces added for clarity) is two glyph
// runs: one italics and one regular. The first run contains 3 glyphs and the
// second run contains 2 glyphs.
// Note that " He *ll* o " is 3 runs even though "He" and "o" have the same font.
for (CFIndex runIndex = 0; runIndex < CFArrayGetCount(runArray); runIndex++)
{
// Get the font for this glyph run.
CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runArray, runIndex);
CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName);
// This glyph run contains one or more glyphs (letters etc.)
for (CFIndex runGlyphIndex = 0; runGlyphIndex < CTRunGetGlyphCount(run); runGlyphIndex++)
{
// Read the glyph itself and it position from the glyph run.
CFRange glyphRange = CFRangeMake(runGlyphIndex, 1);
CGGlyph glyph;
CGPoint position;
CTRunGetGlyphs(run, glyphRange, &glyph);
CTRunGetPositions(run, glyphRange, &position);
// Create a CGPath for the outline of the glyph
CGPathRef letter = CTFontCreatePathForGlyph(runFont, glyph, NULL);
// Translate it to its position.
CGAffineTransform t = CGAffineTransformMakeTranslation(position.x, position.y);
// Add the glyph to the
CGPathAddPath(letters, &t, letter);
CGPathRelease(letter);
}
}
CFRelease(line);
与常规的 UIView 坐标系相比,核心文本坐标系是颠倒的,因此我会翻转路径以匹配我们在屏幕上看到的内容。
// Transform the path to not be upside down
CGAffineTransform t = CGAffineTransformMakeScale(1, -1); // flip 1
CGSize pathSize = CGPathGetBoundingBox(letters).size;
t = CGAffineTransformTranslate(t, 0, -pathSize.height); // move down
// Create the final path by applying the transform
CGPathRef finalPath = CGPathCreateMutableCopyByTransformingPath(letters, &t);
// Clean up all the unused path
CGPathRelease(letters);
self.textPath = finalPath;
现在我有了标签文本的完整 CGPath。
覆盖pointInside:withEvent:
要自定义标签认为在其内部的点,我会覆盖 point inside 并让它检查该点是否在文本路径内。 UIKit 的其他部分会调用这个方法进行命中测试。
// Override -pointInside:withEvent to determine that ourselves.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// Check if the points is inside the text path.
return CGPathContainsPoint(self.textPath, NULL, point, NO);
}
正常的触摸处理
现在一切都已设置为可以使用正常的触摸处理。我在 NIB 中的标签上添加了一个点击识别器,并将其连接到我的视图控制器中的一个方法。
- (IBAction)labelWasTouched:(UITapGestureRecognizer *)sender {
NSLog(@"LABEL!");
}
仅此而已。如果您一直滚动到这里并且不想获取不同的代码并将它们粘贴在一起,我有the entire .m file in a Gist that you can download and use。
请注意,与触摸精度(44 像素)相比,大多数字体非常非常细,当触摸被视为“未命中”时,您的用户很可能会感到非常沮丧。话虽如此:编码愉快!
更新:
为了对用户更好一点,您可以描边用于命中测试的文本路径。这提供了一个更大的区域,可以点击,但仍然给人一种您正在点击文本的感觉。
CGPathRef endPath = CGPathCreateMutableCopyByTransformingPath(letters, &t);
CGMutablePathRef finalPath = CGPathCreateMutableCopy(endPath);
CGPathRef strokedPath = CGPathCreateCopyByStrokingPath(endPath, NULL, 7, kCGLineCapRound, kCGLineJoinRound, 0);
CGPathAddPath(finalPath, NULL, strokedPath);
// Clean up all the unused paths
CGPathRelease(strokedPath);
CGPathRelease(letters);
CGPathRelease(endPath);
self.textPath = finalPath;
现在下图中的橙色区域也可以点击了。这仍然感觉就像您在触摸文本,但对您的应用程序的用户来说不那么烦人。
如果您愿意,您可以更进一步,使其更容易点击文本,但在某些时候会感觉整个标签都是可点击的。