2

I have been beating my head again the wall with this one. I have an arrow that is based upon this post. When I tap the view containing the arrow, I need to know if the arrow layer contains the touchpoint. I've searched for two days for a solution, but haven't found any that works yet. If someone could point me in the right direction, it would be appreciated a thousand times over.

@implementation ArrowView
{
    CGFloat headLength;
    UIBezierPath *path;
}

- (id) initWithFrame:(CGRect)frame withColor: (UIColor *) color withWeight: (CGFloat) weight withStartPoint: (CGPoint) startPoint withEndPoint: (CGPoint) endPoint {

    if (self = [super initWithFrame:frame]) {
        _arrowColor = color;
        _weight = weight;
        _startPoint = startPoint;
        _endPoint = endPoint;

        self.userInteractionEnabled = YES;

        UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapped:)];
        [self addGestureRecognizer:tap];
    }

    return self;
}

- (void) tapped: (UITapGestureRecognizer *) sender {
    CGPoint location = [sender locationInView: sender.view];

    if ([_arrowLayer containsPoint:location]) {
        [_delegate arrowTouched:self];
    }
}

- (void) drawRect:(CGRect)rect {
    [super drawRect:rect];

    [_arrowLayer removeFromSuperlayer];
    [_bottomButtonLayer removeFromSuperlayer];
    [_topButtonLayer removeFromSuperlayer];

    _tailWidth = 5 + _weight;
    _headWidth = 25 + _weight;
    headLength = 40;

    path = [UIBezierPath dqd_bezierPathWithArrowFromPoint:(CGPoint)_startPoint
                                                  toPoint:(CGPoint)_endPoint
                                                tailWidth:(CGFloat)_tailWidth
                                                headWidth:(CGFloat)_headWidth
                                               headLength:(CGFloat)headLength];
    [path setLineWidth:_weight];

    CGFloat width = _endPoint.x - _startPoint.x;
    CGFloat height = _endPoint.y - _startPoint.y;

    CGFloat offset = 5;
    if (ABS(width) < offset) {
        width = width > 0 ? offset : -offset;
    }

    if (ABS(height) < offset) {
        height = height > 0 ? offset : -offset;
    }

    _arrowLayer = [CAShapeLayer layer];
    _arrowLayer.bounds = CGRectMake(_startPoint.x, _startPoint.y, width, height);
    _arrowLayer.position = CGPointMake(((_endPoint.x - _startPoint.x) / 2), (_endPoint.y - _startPoint.y) / 2);

    _arrowLayer.path = path.CGPath;
    _arrowLayer.fillColor = _arrowColor.CGColor;
    //_arrowLayer.backgroundColor = [UIColor greenColor].CGColor;

    if (!_isSelected) {
        _arrowLayer.strokeColor = _arrowColor.CGColor;
    } else {
        if (_arrowColor != [UIColor blackColor]) {
            _arrowLayer.strokeColor = [UIColor blackColor].CGColor;
        } else {
            _arrowLayer.strokeColor = [UIColor whiteColor].CGColor;
        }
    }

    _arrowLayer.borderColor = [UIColor whiteColor].CGColor;
    _arrowLayer.borderWidth = 1;

    [self.layer addSublayer:_arrowLayer];
}

- (void) setIsSelected: (BOOL) isSelected {
    _isSelected = isSelected;
    [self setNeedsDisplay];
}

- (void) updateStartPoint: (CGPoint) startPoint {
    _startPoint = startPoint;
    [self setNeedsDisplay];
}

- (void) updateEndPoint: (CGPoint) endPoint {
    _endPoint = endPoint;
    [self setNeedsDisplay];
}

- (void) updateColor: (UIColor *) color {
    _arrowColor = color;
    [self setNeedsDisplay];
}

- (void) updateWeight: (CGFloat) weight {
    _weight = weight;
    [self setNeedsDisplay];
}

@end

#define kArrowPointCount 7

@implementation UIBezierPath (dqd_arrowhead)

+ (UIBezierPath *)dqd_bezierPathWithArrowFromPoint:(CGPoint)startPoint toPoint:(CGPoint)endPoint tailWidth:(CGFloat)tailWidth headWidth:(CGFloat)headWidth headLength:(CGFloat)headLength {

    CGFloat length = hypotf(endPoint.x - startPoint.x, endPoint.y - startPoint.y);
    CGPoint points[kArrowPointCount];
    [self dqd_getAxisAlignedArrowPoints:points forLength:length tailWidth:tailWidth headWidth:headWidth headLength:headLength];
    CGAffineTransform transform = [self dqd_transformForStartPoint:startPoint endPoint:endPoint length:length];
    CGMutablePathRef cgPath = CGPathCreateMutable();
    CGPathAddLines(cgPath, &transform, points, sizeof points / sizeof *points);
    CGPathCloseSubpath(cgPath);
    UIBezierPath *uiPath = [UIBezierPath bezierPathWithCGPath:cgPath];
    CGPathRelease(cgPath);

    return uiPath;
}

+ (void)dqd_getAxisAlignedArrowPoints:(CGPoint[kArrowPointCount])points
                            forLength:(CGFloat)length
                            tailWidth:(CGFloat)tailWidth
                            headWidth:(CGFloat)headWidth
                           headLength:(CGFloat)headLength {
    CGFloat tailLength = length - headLength;
    points[0] = CGPointMake(0, tailWidth / 2);
    points[1] = CGPointMake(tailLength, tailWidth / 2);
    points[2] = CGPointMake(tailLength, headWidth / 2);
    points[3] = CGPointMake(length, 0);
    points[4] = CGPointMake(tailLength, -headWidth / 2);
    points[5] = CGPointMake(tailLength, -tailWidth / 2);
    points[6] = CGPointMake(0, -tailWidth / 2);
}

+ (CGAffineTransform)dqd_transformForStartPoint:(CGPoint)startPoint
                                       endPoint:(CGPoint)endPoint
                                         length:(CGFloat)length {
    CGFloat cosine = (endPoint.x - startPoint.x) / length;
    CGFloat sine = (endPoint.y - startPoint.y) / length;
    return (CGAffineTransform){ cosine, sine, -sine, cosine, startPoint.x, startPoint.y };
}

@end
Community
  • 1
  • 1
Olan Hall
  • 29
  • 6
  • 1
    Did you try with `CGPathContainsPoint(_arrowLayer.path, &transf, location, NO)` `transf` is the CGAffineTransform of _arrowLayer – tuledev Sep 03 '15 at 07:16
  • I did, but it always returns false. – Olan Hall Sep 04 '15 at 14:41
  • I've also tried this: CGPoint transformedPoint = CGPointApplyAffineTransform(location, transform); if ([_arrowLayer containsPoint:transformedPoint]) { [_delegate arrowTouched:self]; } – Olan Hall Sep 04 '15 at 15:03

2 Answers2

1

I found an alternate solution. SpaceDog got me thinking about the situation differently. I just need to know if an area with color is touched. I found this post and translated it to Objective C. It works beautifully. While I would prefer the detection of the point was within the arrow, this should work for my purposes just as well. Thanks for y'all's reply. I appreciate it.

Post I got the code below from.

- (void) tapped: (UITapGestureRecognizer *) sender {
    CGPoint location = [sender locationInView: sender.view];
    CGFloat p = [self alphaFromPoint:location];
    if (p > 0) {
        [_delegate arrowTouched:self];
    }
}

- (CGFloat) alphaFromPoint: (CGPoint) point {
    UInt8 pixel[4] = {0,0,0,0};
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGBitmapInfo alphaInfo = (CGBitmapInfo)kCGImageAlphaPremultipliedLast;


    CGContextRef context = CGBitmapContextCreate(&pixel, 1, 1, 8, 4, colorSpace, alphaInfo);

    CGContextTranslateCTM(context, -point.x, -point.y);

    [self.layer renderInContext:context];
    CGFloat floatAlpha = pixel[3];
    return floatAlpha;
}
Community
  • 1
  • 1
Olan Hall
  • 29
  • 6
0

If the view has the same size as the arrow and tapped: is being triggered it means you have tapped the view with the arrow.

You don't need to this:

- (void) tapped: (UITapGestureRecognizer *) sender {
    CGPoint location = [sender locationInView: sender.view];

    if ([_arrowLayer containsPoint:location]) {
        [_delegate arrowTouched:self];
    }
}

you can have this:

- (void) tapped: (UITapGestureRecognizer *) sender {

  NSLog(@"arrow was tapped");

}
Duck
  • 34,902
  • 47
  • 248
  • 470
  • The problem comes when the arrow is at a 45 degree angle. The view is considerable larger than the arrow itself. This is why I need to know when the arrow itself is touched, but not when the area around the arrow is touched. – Olan Hall Sep 04 '15 at 14:51
  • this is because you are building a rectangle to contain a rotated arrow. Instead you should simple create a rectangle the size of the arrow and rotate the rectangle. You may do this work but your approach is wrong. You are wasting GPU cycles writing a view that is larger than necessary. – Duck Sep 05 '15 at 15:59
  • I completely agree my implementation isn't the optimal solution. I spent 5 hours last night trying your suggestion with CGAffineTransform try to make is rotate around center point. I was not successful and gave up. It is something I plan on revisiting when the project is nearly complete. If you have any examples of rotating a view around a central point, please share them. I spent many hours searching and didn't find anything that helped. – Olan Hall Sep 06 '15 at 19:14