在iOS中绘制线条时点击检测

问题描述:

我想允许用户以这样的方式绘制曲线,即没有线条可以穿过另一条线或甚至自身。绘制曲线是没有问题的,我甚至发现我可以创建一条封闭的路径,并且通过追踪线路的节点并返回然后关闭路径,仍然非常像线条一样。在iOS中绘制线条时点击检测

不幸的是,iOS只提供了一个测试点,是否包含在一个封闭的路径(containsPoint:和CGPathContainsPoint)中。不幸的是,用户可以非常容易地将手指移动得足够快,使得触摸点落在现有路径的两侧而不实际上被该路径包含,因此测试触摸点是非常没有意义的。

我找不到路径的任何“交点”方法。

有关如何完成此任务的其他想法?

+0

这个问题类似于另一个SO问题。 http://*.com/questions/1021801/cgpathref-intersection那些答案建议看看每个单独的像素,这将是缓慢的。您可以通过myBezierPath从您的UIBezierPath对象中获取CGPathRef.CGPath – Andrew

+0

对类似问题的好处。我正在研究比较连续位图的方法。一旦我有演示代码,我会把它放在这里。同时,我也会查看关于这个问题的答案。 – EFC

嗯,我确实想出了一个办法来做到这一点。这是不完美的,但我认为其他人可能想看到这个技术,因为这个问题已经提出了几次。我使用的技术将所有要测试的项目绘制到位图上下文中,然后将进度线的新段绘制到另一个位图上下文中。这些上下文中的数据使用按位运算符进行比较,如果发现任何重叠,则声明一次。

这种技术背后的想法是测试新绘制的线条的每一个部分与所有先前绘制的线条以及甚至对同一线条的较早部分。换句话说,这种技术可以检测到一条线穿过另一条线时,还可以检测到线穿过另一条线时。

演示此技术的示例应用程序可用:LineSample.zip

命中测试的核心是在我的LineView对象中完成的。这里有两个关键方法:

- (CGContextRef)newBitmapContext { 

    // creating b&w bitmaps to do hit testing 
    // based on: http://robnapier.net/blog/clipping-cgrect-cgpath-531 
    // see "Supported Pixel Formats" in Quartz 2D Programming Guide 
    CGContextRef bitmapContext = 
    CGBitmapContextCreate(NULL, // data automatically allocated 
          self.bounds.size.width, 
          self.bounds.size.height, 
          8, 
          self.bounds.size.width, 
          NULL, 
          kCGImageAlphaOnly); 
    CGContextSetShouldAntialias(bitmapContext, NO); 
    // use CGBitmapContextGetData to get at this data 

    return bitmapContext; 
} 


- (BOOL)line:(Line *)line canExtendToPoint:(CGPoint) newPoint { 

    // Lines are made up of segments that go from node to node. If we want to test for self-crossing, then we can't just test the whole in progress line against the completed line, we actually have to test each segment since one segment of the in progress line may cross another segment of the same line (think of a loop in the line). We also have to avoid checking the first point of the new segment against the last point of the previous segment (which is the same point). Luckily, a line cannot curve back on itself in just one segment (think about it, it takes at least two segments to reach yourself again). This means that we can both test progressive segments and avoid false hits by NOT drawing the last segment of the line into the test! So we will put everything up to the last segment into the hitProgressLayer, we will put the new segment into the segmentLayer, and then we will test for overlap among those two and the hitTestLayer. Any point that is in all three layers will indicate a hit, otherwise we are OK. 

    if (line.failed) { 
     // shortcut in case a failed line is retested 
     return NO; 
    } 
    BOOL ok = YES; // thinking positively 

    // set up a context to hold the new segment and stroke it in 
    CGContextRef segmentContext = [self newBitmapContext]; 
    CGContextSetLineWidth(segmentContext, 2); // bit thicker to facilitate hits 
    CGPoint lastPoint = [[[line nodes] lastObject] point]; 
    CGContextMoveToPoint(segmentContext, lastPoint.x, lastPoint.y); 
    CGContextAddLineToPoint(segmentContext, newPoint.x, newPoint.y); 
    CGContextStrokePath(segmentContext); 

    // now we actually test 
    // based on code from benzado: http://*.com/questions/6515885/how-to-do-comparisons-of-bitmaps-in-ios/6515999#6515999 
    unsigned char *completedData = CGBitmapContextGetData(hitCompletedContext); 
    unsigned char *progressData = CGBitmapContextGetData(hitProgressContext); 
    unsigned char *segmentData = CGBitmapContextGetData(segmentContext); 

    size_t bytesPerRow = CGBitmapContextGetBytesPerRow(segmentContext); 
    size_t height = CGBitmapContextGetHeight(segmentContext); 
    size_t len = bytesPerRow * height; 

    for (int i = 0; i < len; i++) { 
     if ((completedData[i] | progressData[i]) & segmentData[i]) { 
      ok = NO; 
      break; 
     } 
    } 

    CGContextRelease(segmentContext); 

    if (ok) { 
     // now that we know we are good to go, 
     // we will add the last segment onto the hitProgressLayer 
     int numberOfSegments = [[line nodes] count] - 1; 
     if (numberOfSegments > 0) { 
      // but only if there is a segment there! 
      CGPoint secondToLastPoint = [[[line nodes] objectAtIndex:numberOfSegments-1] point]; 
      CGContextSetLineWidth(hitProgressContext, 1); // but thinner 
      CGContextMoveToPoint(hitProgressContext, secondToLastPoint.x, secondToLastPoint.y); 
      CGContextAddLineToPoint(hitProgressContext, lastPoint.x, lastPoint.y); 
      CGContextStrokePath(hitProgressContext); 
     } 
    } else { 
     line.failed = YES; 
     [linesFailed addObject:line]; 
    } 
    return ok; 
} 

我很乐意听到建议或看到改进。首先,仅检查新分段的边界矩形而不是整个视图会快很多。

+2

公平的警告:我已经在示例应用程序中发现了一些错误,所以请确保您注意自己的实现。基本的技术似乎可行,只是可以改进的一些实施问题。我会更多地调整样本并保持更新,但我的主要焦点将在其他地方。 – EFC

+0

嗨@EFC,我对社区和新手iOS程序员有点新鲜,你能指出我具体在哪里代码防止相交吗?我只需要那部分。 – EdSniper

+0

为了防止相交,我只是想查看旧段和新段之间是否有任何位相同。 'if((completedData [i] | progressData [i])&segmentData [i]){'line是什么实际测试。此测试来自http://*.com/a/6515999/383737。 – EFC