tl; dr:您可以点击测试文本的路径。要点在这里可用。
我将采用的方法是检查点击点是否在文本路径内。在详细介绍之前,让我先概述一下这些步骤。
- 子类 UILabel
- 使用Core Text获取文本的CGPath
- 覆盖
pointInside:withEvent:
以能够确定是否应在内部考虑某个点。
- 使用任何“正常”的触摸处理,例如点击手势识别器来了解何时进行了点击。
这种方法的最大优点是它精确地遵循字体,并且您可以修改路径以增加“可点击”区域,如下所示。黑色和橙色部分都是可点击的,但标签中只会绘制黑色部分。
子类 UILabel
我创建了一个UILabel
被调用的子类,TextHitTestingLabel
并为文本路径添加了一个私有属性。
@interface TextHitTestingLabel (/*Private stuff*/)
@property (assign) CGPathRef textPath;
@end
由于 iOS 标签可以有 atext
或 an 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!");
}
仅此而已。如果您一直滚动到此处并且不想将不同的代码片段粘贴在一起,我将整个 .m 文件放在 Gist 中,您可以下载和使用。
请注意,与触摸的精度(44px)相比,大多数字体非常非常薄,当触摸被认为是“未命中”时,您的用户很可能会非常沮丧。话虽这么说:快乐编码!
更新:
为了对用户更好一点,您可以描边用于命中测试的文本路径。这提供了一个更大的区域,可以点击,但仍然给人一种您正在点击文本的感觉。
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;
现在,下图中的橙色区域也可以点击了。这仍然感觉就像您在触摸文本,但对您的应用程序的用户来说不那么烦人。
如果你愿意,你可以更进一步,让它更容易点击文本,但在某些时候,你会感觉整个标签都是可点击的。