使用 CoreText 和触摸来创建可点击的动作

Posted

技术标签:

【中文标题】使用 CoreText 和触摸来创建可点击的动作【英文标题】:Using CoreText and touches to create a clickable action 【发布时间】:2010-09-27 09:53:55 【问题描述】:

我在视图中有一些代码,它使用 CoreText 绘制了一些属性文本。在此,我正在搜索 url 并将它们设为蓝色。我们的想法是不要仅仅为了获得可点击的链接而引入UIWebView 的所有开销。一旦用户点击该链接(而不是整个表格视图单元格),我想触发一个委托方法,然后该方法将用于呈现一个模式视图,其中包含一个指向该 url 的 web 视图。

我将路径和字符串本身保存为视图的实例变量,绘图代码发生在-drawRect: 中(为简洁起见,我将其省略了)。

然而,我的触摸处理程序虽然不完整,但并没有打印出我期望的内容。如下:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event

    UITouch *touch = [touches anyObject];
    CGPoint point = [touch locationInView:self];
    CGContextRef context = UIGraphicsGetCurrentContext();

    NSLog(@"attribString = %@", self.attribString);
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)self.attribString);
    CTFrameRef ctframe = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, self.attribString.length), attribPath, NULL);

    CFArrayRef lines = CTFrameGetLines(ctframe);
    for(CFIndex i = 0; i < CFArrayGetCount(lines); i++)
    
        CTLineRef line = CFArrayGetValueAtIndex(lines, i);
        CGRect lineBounds = CTLineGetImageBounds(line, context);

        // Check if tap was on our line
        if(CGRectContainsPoint(lineBounds, point))
        
            NSLog(@"Tapped line");
            CFArrayRef runs = CTLineGetGlyphRuns(line);
            for(CFIndex j = 0; j < CFArrayGetCount(runs); j++)
            
                CTRunRef run = CFArrayGetValueAtIndex(runs, j);
                CFRange urlStringRange = CTRunGetStringRange(run); 
                CGRect runBounds = CTRunGetImageBounds(run, context, urlStringRange);

                if(CGRectContainsPoint(runBounds, point))
                
                    NSLog(@"Tapped run");
                    CFIndex* buffer = malloc(sizeof(CFIndex) * urlStringRange.length);
                    CTRunGetStringIndices(run, urlStringRange, buffer);
                    // TODO: Map the glyph indices to indexes in the string & Fire the delegate
                
            
        
    

目前这不是最漂亮的代码,我仍在努力让它工作,所以请原谅代码质量。

我遇到的问题是,当我在链接外部点击时,我期望会发生的事情发生了:什么都没有被触发。

但是,如果我点击链接所在的同一行,我希望"Tapped line" 会被打印出来,这不会发生,如果我点击,我希望"Tapped line""Tapped run" 都会被打印出来网址。

我不确定在哪里可以进一步解决这个问题,我为解决这个问题而查看的资源是 Cocoa 特定的(几乎完全不适用),或者缺乏关于这个特定案例的信息。

我很乐意参考文档,其中详细说明了如何正确地检测是否在绘制代码的核心文本范围内发生了触摸,但在这一点上,我只想解决这个问题,所以任何帮助将不胜感激。

更新:我已将问题缩小到坐标问题。我已经翻转了坐标(而不是如上所示),我遇到的问题是触摸寄存器如我所料,但坐标空间被翻转,我似乎无法将其翻转回来。

【问题讨论】:

你有没有考虑到CoreText使用的是翻转坐标系?现在在我看来,您正在比较两个不同坐标系中的事物。 另外,像这样在每次触摸时重新创建框架设置器是个坏主意。创建框架设置器非常昂贵,因此您应该在第一次绘制或设置文本时将其缓存。 我的开发方法很简单:1)让它工作; 2)做对了; 3)让它快速/干净。当我们得到#1 时,让我们处理#2 :) 关于坐标系,是的,我确实意识到了这一点,并且有一段时间我没有处理它。在现在的代码中,在咨询了一位同事之后,他让我明白了这一点,它至少现在检测线路上的水龙头,而不是运行。仍在努力解决这个问题。 【参考方案1】:

我刚刚这样做是为了从触摸位置获取字符串字符索引。在这种情况下,行号是 i:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 
    NSLog(@"touch ended");

    UITouch* touch = [touches anyObject];

    CGPoint pnt = [touch locationInView:self];

    CGPoint reversePoint = CGPointMake(pnt.x, self.frame.size.height-pnt.y);

    CFArrayRef lines = CTFrameGetLines(ctFrame);

    CGPoint* lineOrigins = malloc(sizeof(CGPoint)*CFArrayGetCount(lines));

    CTFrameGetLineOrigins(ctFrame, CFRangeMake(0,0), lineOrigins);

    for(CFIndex i = 0; i < CFArrayGetCount(lines); i++)
    
        CTLineRef line = CFArrayGetValueAtIndex(lines, i);

        CGPoint origin = lineOrigins[i];
        if (reversePoint.y > origin.y) 
            NSInteger index = CTLineGetStringIndexForPosition(line, reversePoint);
            NSLog(@"index %d", index);
            break;
        
    
    free(lineOrigins);

【讨论】:

谢谢尼克你救了我! :) 这很好用,我可以将其与我为可点击单词存储的一组范围进行比较。这比我的旧方法快得多 - 谢谢:) 如何获取 ctFrame? 它是当前视图中的核心文本框架的 CTFrameRef 对象。在我的实现中,它是一个成员变量,使用 CTFramesetterCreateFrame 创建 嗨,是的 - 在核心文本中(如在 OSX 开发中)高度位置是反转的,所以左下角是 0,0 - 而在 UIKit 中左上角是 0,0!【参考方案2】:

您可以尝试将您的文本绘图添加到 CALayer 中,将此新图层添加为您的视图图层的子图层并在您的 touchesEnded 中对其进行测试。如果这样做,您应该能够通过使图层大于绘制的文本来创建更大的可触摸区域。

// hittesting 
UITouch *touch = [[event allTouches] anyObject];
touchedLayer = (CALayer *)[self.layer hitTest:[touch locationInView:self]];

【讨论】:

然后我会遇到另一个问题,即必须将我的文本分成两个不同的层——我要触发代理的层,然后是其余的层。我遇到的问题是,如果 URL 位于一行的中间并且前后有文本,会发生什么?然后我必须分成许多不同的层。如果我渲染的文本组中有几个 URL,它可能会变得很麻烦。我宁愿只获取属性字符串中所有项目的列表,我已将其涂成蓝色,并获取它们的边界框,查看用户是否在其中点击并使用该 URL 调用委托。 没错,这可能是个问题。也许我没有正确理解你的问题。如何保持文本不变并覆盖一个可点击的图层来捕捉这些触摸? 我仍然需要知道确切的放置位置——这意味着我仍然需要获取 URL 的正确位置。这让我回到了第一方。 :) 我的问题是我有文本,我知道链接在文本中的哪个位置,但是在我绘制它之后,我需要找到那个链接。我一开始只是试图找到 ANY 运行的字形,但没有任何东西可以触发到我的 NSLog() 显示我已经击中一条线,或者击中了一串字符。由于无法识别我点击了一些文字,我没有希望找出我点击的文字是否是 URL。【参考方案3】:

Swift 3 版本的 Nick H247 的回答:

var ctFrame: CTFrame?

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) 
    guard let touch = touches.first, let ctFrame = self.ctFrame else  return 

    let pnt = touch.location(in: self.view)
    let reversePoint = CGPoint(x: pnt.x, y: self.frame.height - pnt.y)
    let lines = CTFrameGetLines(ctFrame) as [AnyObject] as! [CTLine]

    var lineOrigins = [CGPoint](repeating: .zero, count: lines.count)
    CTFrameGetLineOrigins(ctFrame, CFRange(location: 0, length: 0), &lineOrigins)

    for (i, line) in lines.enumerated() 
        let origin = lineOrigins[i]

        if reversePoint.y > origin.y 
            let index = CTLineGetStringIndexForPosition(line, reversePoint)
            print("index \(index)")
            break
        
    

【讨论】:

以上是关于使用 CoreText 和触摸来创建可点击的动作的主要内容,如果未能解决你的问题,请参考以下文章

css 笔记:移动端触摸事件 - 点击延迟的证明和“点穿”点穿 - 3.触摸动作

使用 UIButton 进行触摸检测

将标签移动到目标c中的矩形后如何从标签中删除触摸动作

如何防止在 UINavigationbar 上同时触摸按钮的双重动作?

CoreText实现图文混排之点击事件

当按下另一个可触摸的不透明度时,一个可触摸的不透明度不会按下