在iOS中绘制线条时命中检测

时间:2011-06-26 05:46:27

标签: ios cocoa-touch hittest uibezierpath

我想允许用户绘制曲线,使得任何线都不能穿过另一条线甚至自身。绘制曲线是没有问题的,我甚至发现我可以通过追踪线的节点向前和向后然后关闭路径来创建一个闭合的路径,并且仍然是线条状的。

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

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

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

2 个答案:

答案 0 :(得分:6)

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

这种技术背后的想法是测试新绘制的线的每个片段与所有先前绘制的线,甚至是相同线的早期片段。换句话说,这种技术将检测线何时穿过另一条线以及何时穿过它自己。

展示该技术的示例应用程序可用: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://stackoverflow.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;
}

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

答案 1 :(得分:0)

迅速4,答案基于CGPath Hit Testing - Ole Begemann(2012年)

来自Ole Begemann博客:

contains(point: CGPoint)
  

如果您想在整个区域进行测试,此功能很有用   路径覆盖。因此,包含(点:CGPoint)   未封闭的路径,因为那些内部没有   填充。

copy(strokingWithWidth lineWidth: CGFloat, lineCap: CGLineCap, lineJoin: CGLineJoin, miterLimit: CGFloat, transform: CGAffineTransform = default) -> CGPath
  

此功能创建一个仅覆盖的镜像点击目标对象   路径的描边区域。当用户点击屏幕时,我们   遍历敲击目标而不是实际形状。


我的代码解决方案

我使用链接到功能tap()的UITapGestureRecognizer:

var bezierPaths = [UIBezierPath]()   // containing all lines already drawn
var tappedPaths = [CAShapeLayer]()

@IBAction func tap(_ sender: UITapGestureRecognizer) {        
    let point = sender.location(in: imageView)

    for path in bezierPaths {
        // create tapTarget for path
        if let target = tapTarget(for: path) {
            if target.contains(point) {
                tappedPaths.append(layer)
            }
        }
    }
}

fileprivate func tapTarget(for path: UIBezierPath) -> UIBezierPath {

    let targetPath = path.copy(strokingWithWidth: path.lineWidth, lineCap: path..lineCapStyle, lineJoin: path..lineJoinStyle, miterLimit: path.miterLimit)

    return UIBezierPath.init(cgPath: targetPath)
}
相关问题