【问题标题】:How to check if UILabel text was touched?如何检查 UILabel 文本是否被触摸?
【发布时间】:2013-06-20 01:03:44
【问题描述】:

我想检查我的UILabel 是否被触摸。但我需要的还不止这些。文字被触动了吗?现在,如果使用此触摸UILabel 框架,我只会得到真/假:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [[event allTouches] anyObject];
    if (CGRectContainsPoint([self.currentLetter frame], [touch locationInView:self.view]))
    {
        NSLog(@"HIT!");
    }
}

有什么方法可以检查吗?只要我触摸UILabel 中字母之外的某个地方,我就希望返回 false。

我想知道实际的黑色渲染“文本像素”何时被触摸。

谢谢!

【问题讨论】:

  • 如果需要,我可以用任何其他对象替换 UILabel,即使图像也可以,具有透明背景的 PNG 可以以某种方式工作吗?
  • 什么是currentLetter?什么是“信外某处”?
  • 也许让 uibutton 没有背景?
  • 你的意思是文本占用的空间,标签更大还是实际像素被渲染为黑色?
  • 你不觉得这些字母对人的手指来说太细了吗?

标签: iphone ios objective-c ipad uilabel


【解决方案1】:

tl;dr: 你可以点击测试文本的路径。 Gist is available here.


我会采用的方法是检查点击点是否在文本路径内。在详细介绍之前,让我先概述一下这些步骤。

  1. 子类 UILabel
  2. 使用Core Text获取文本的CGPath
  3. 覆盖 pointInside:withEvent: 以能够确定是否应将点视为内部。
  4. 使用任何“正常”触摸处理(例如点击手势识别器)来了解点击的时间。

这种方法的最大优点是它可以精确地跟随字体,并且您可以修改路径以扩大“可点击”区域,如下所示。黑色和橙色部分都是可点击的,但标签中只会绘制黑色部分。

子类 UILabel

我创建了一个名为 TextHitTestingLabelUILabel 的子类,并为文本路径添加了一个私有属性。

@interface TextHitTestingLabel (/*Private stuff*/)
@property (assign) CGPathRef textPath;
@end

由于 iOS 标签可以有 textattributedText,所以我将这两个方法子类化并让它们调用一个方法来更新文本路径。

- (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;

现在下图中的橙色区域也可以点击了。这仍然感觉就像您在触摸文本,但对您的应用程序的用户来说不那么烦人。

如果您愿意,您可以更进一步,使其更容易点击文本,但在某些时候会感觉整个标签都是可点击的。

【讨论】:

  • 我认为这是对您可能自己呈现的文本进行一般命中测试的最佳技术。但是与 UILabel 一起使用时,它有一些问题: 1) 在我将标签的边界框调整为非常紧密地包含文本之前,我无法让它工作。如果标签框架更高(或只是默认 sizeToFit 高度),则命中测试与 y 轴上呈现的内容不一致。 2) 不考虑 UILabel 可能应用的任何自动截断。 3) 不确定 Autoshrink 支持 - 似乎不太可能。 4) 不能在 Xcode 4.6 中编译(在某个 Developer Preview 中编译...)
  • 我看到的编译错误是由于传递给 CTLineCreateWithAttributedString 的参数的隐式桥接转换。将 __bridge 添加到演员阵容中即可。
  • @TomSwift 这是一些非常好的反馈。谢谢你。我会看看我能做些什么来解决这些问题。
【解决方案2】:

据我了解,问题是检测何时在构成 UILabel 中的文本的字形之一上发生点击(触摸)。如果触摸落在任何字形路径之外,则不计算在内。

这是我的解决方案。它假定一个名为 _label 的 UILabel* ivar 和一个与包含标签的视图关联的 UITapGestureRecognizer

- (IBAction) onTouch: (UITapGestureRecognizer*) tgr
{
    CGPoint p = [tgr locationInView: _label];

    // in case the background of the label isn't transparent...
    UIColor* labelBackgroundColor = _label.backgroundColor;
    _label.backgroundColor = [UIColor clearColor];

    // get a UIImage of the label
    UIGraphicsBeginImageContext( _label.bounds.size );
    CGContextRef c = UIGraphicsGetCurrentContext();
    [_label.layer renderInContext: c];
    UIImage* i = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    // restore the label's background...
    _label.backgroundColor = labelBackgroundColor;

    // draw the pixel we're interested in into a 1x1 bitmap
    unsigned char pixel = 0x00;
    c = CGBitmapContextCreate(&pixel,
                              1, 1, 8, 1, NULL,
                              kCGImageAlphaOnly);
    UIGraphicsPushContext(c);
    [i drawAtPoint: CGPointMake(-p.x, -p.y)];
    UIGraphicsPopContext();
    CGContextRelease(c);

    if ( pixel != 0 )
    {
        NSLog( @"touched text" );
    }
}

【讨论】:

  • 这工作得很好,我花了 5 分钟来实施。我很确定线程中还有其他很好的替代建议,不幸的是我不在我可以测试所有这些建议的位置。我给你答案@TomSwift,因为你的代码似乎符合我的需要。
  • 这是一个很棒的 sn-p 代码,Tom!这种检测特定像素的 Alpha 通道以确定用户是否选择字形的概念很酷。你知道这方面的任何实际应用吗?
【解决方案3】:

您可以使用UIGestureRecognizerhttp://developer.apple.com/library/ios/#documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/GestureRecognizer_basics/GestureRecognizer_basics.html

具体来说,我猜你想使用UITapGestureRecognizer。如果您想识别何时触摸了文本框,那么最简单的方法是使用[yourLabel sizeToFit] 使框的大小适合文本。

无论如何,我会使用UIButton,这是最简单的选择。

如果您只需要检测实际文本而不是整个UITextField 框架 被点击,那么它变得更加困难。一种方法是检测用户点击的像素的暗度,但这涉及到一些丑陋的代码。无论如何,取决于您的应用程序中的预期交互可以解决。检查这个 SO 问题:

iOS -- detect the color of a pixel?

我会考虑到并非所有渲染的像素都是 100% 黑色的,所以我会使用阈值来获得更好的结果。

【讨论】:

  • 你自己说的,设置帧大小。我不想知道用户何时点击框架,我想知道用户何时点击实际文本,黑色渲染像素形成字母。
  • @JoakimBörjesson 答案已更新 ;-) 不是最佳解决方案(远非如此),但试一试 :-)
【解决方案4】:

我想他想知道标签内的字母是否被触摸,而不是标签的其他部分。由于您愿意使用透明图像来实现这一点,我建议,例如您有透明背景的字母“A”,如果字母的颜色单调,那么在这种情况下说红色,你可以抓住UIImage 的 CGImage,获取提供者并将其渲染为位图并采样被触摸点的颜色是否为红色。对于其他颜色,您可以简单地使用在线图像编辑器对该颜色进行采样并获取其 RGB 值并进行检查。

【讨论】:

  • 你有我如何检查被触摸点颜色的例子吗?我不能也验证 UILabel 文本的文本颜色吗?请详细说明一下。
【解决方案5】:

您可以使用 UIButton 代替标签:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    UIButton *tmpButton = [[UIButton alloc] initWithFrame:CGRectMake(50, 50, 100, 20)];
    [tmpButton setTitle:@"KABOYA" forState:UIControlStateNormal];
    [tmpButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    [tmpButton addTarget:self
              action:@selector(buttonPressed:)
    forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:tmpButton];
}

当按下按钮时在这里做一些事情:

-(void)buttonPressed:(UIButton *)sender {
    NSLog(@"Pressed !");
}

希望对你有帮助;)

【讨论】:

  • 例如,如果我写字母 A。当我在字母周围形成的框架(正方形)内触摸时,将触发 uibutton 触摸。我需要的是仅在触摸实际黑色像素时触发事件。如果您在 O 内按下,则它也不应该触发。
【解决方案6】:

假设您要跟踪的 UILabel 实例是 userInteractionEnabled。

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [[event allTouches] anyObject];
    UIView *touchView = touch.view;
    if([touchView isKindOfClass:[UILabel class]]){
        NSLog(@"Touch event occured in Label %@",touchView);
    }
}

【讨论】:

    【解决方案7】:

    首先创建并附加点击手势识别器并允许用户交互:

    UITapGestureRecognizer * tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGesture:)];
    [self.label addGestureRecognizer:tapRecognizer];
    self.label.userInteractionEnabled = YES;
    

    现在实现-tapGesture:

    - (void)tapGesture:(UITapGestureRecognizer *)recognizer
    {
        // Determine point touched
        CGPoint point = [recognizer locationInView:self.label];
    
        // Render UILabel in new context
        UIGraphicsBeginImageContext(self.label.bounds.size);
        CGContextRef context = UIGraphicsGetCurrentContext();
        [self.label.layer renderInContext:context];
    
        // Getting RGBA of concrete pixel
        int bpr = CGBitmapContextGetBytesPerRow(context);
        unsigned char * data = CGBitmapContextGetData(context);
        if (data != NULL)
        {
            int offset = bpr*round(point.y) + 4*round(point.x);
            int red = data[offset+0];
            int green = data[offset+1];
            int blue = data[offset+2];
            int alpha =  data[offset+3];
    
            NSLog(@"%d %d %d %d", alpha, red, green, blue);
    
            if (alpha == 0)
            {
                // Here is tap out of text
            }
            else
            {
                // Here is tap right into text
            }
        }
    
        UIGraphicsEndImageContext();
    }
    

    这将适用于具有透明背景的 UILabel,如果这不是您想要的,您可以将 alpha、red、green、blue 与self.label.backgroundColor...进行比较...

    【讨论】:

      【解决方案8】:

      在 viewDidLoad 中或通过 IB 创建标签,并使用下面的代码和选择器添加 tapGesture,然后当您点击标签时将打印日志(在单点法中:)

      - (void)viewDidLoad
      {
      [super viewDidLoad];    
      UILabel * label = [[UILabel alloc] initWithFrame:CGRectMake(30, 0, 150, 35)];
      label.userInteractionEnabled = YES;
      label.backgroundColor = [UIColor greenColor];
      label.text = @"label";
      label.textAlignment = NSTextAlignmentCenter;
      
      UITapGestureRecognizer * single = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singletap:)];
      [label addGestureRecognizer:single];
      single.numberOfTapsRequired = 1;
      [self.view addSubview:label];
      
      
      }
      -(void) singletap:(id)sender
      {
      NSLog(@"single tap");
      //do your stuff here
      }
      

      如果您发现它,请将其标记为正面 快乐编码

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-02-14
        • 1970-01-01
        • 2016-07-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多