Question

I'm trying to implement a UI element using a custom CALayer inside of a custom UIView.

Basically, the user moves a slider with his/her finger around a circle, so I have a layer for interaction, and then a sublayer of that that is of type CAShapeLayer, which represents the slider itself. I figured the easiest approach to moving the slider around the circle is to just rotate the CAShapeLayer about its z-axis.

Although the slider does visually rotate as expected, when I perform hit tests on touches received, the "hittable" area still resides at the pre-rotated location of the slider. It's as if the visual effect of the CAShapeLayer's rotation is decoupled from the UIBezierPath that is embedded inside of the layer to form the path property of the layer, since I'm using that path in conjunction with CGPathContainsPoint() to recognize touches on the slider.

I'm new to Core Graphics in general, so I'm thinking there might be a property I'm not setting correctly here, but I'm struggling to figure out what it is.

Here is the code:

TouchableLayer.m

@interface TouchableLayer ()
{
    CAShapeLayer *_slider;    // The interactive slider that gets moved around the circle.
}

-(id) initWithPosition:(NSInteger) position // Designated initializer for this layer.
{
    if ( self = [super init] )
    {
        _slider = [CAShapeLayer layer];

        _slider.fillColor = [UIColor blackColor].CGColor;

        [self addSublayer:_slider];
    }

    return self;
}

-(void) setFrame:(CGRect)frame
{
    [super setFrame:frame];

    _slider.frame = frame;

    // This path is currently hardcoded to be in the right starting spot according to
    //    other UI elements, but the magic numbers will go away once I figure out this
    //    rotation issue.
    _slider.path = [UIBezierPath bezierPathWithRect:CGRectMake(self.bounds.size.width-47, self.bounds.size.height/2-5, 30.0f, 10.0f)].CGPath;

}

// Checks if the given touch location was on the slider. Returns YES if it was and NO if it was not.
-(BOOL) checkSliderTouchAtPoint: (CGPoint) point
{
    if (CGPathContainsPoint(_slider.path , NULL, point, NO))
    {
        return YES;
    }
    else
    {
        return NO;
    }
}

// Purpose: Takes the given touch location, determines the angle (in radians) that it forms with respect the center of the screen,
//             and returns that angle on the interval [0-2pi] radians. [0-2pi] radians follows a positive counterclockwise path.
-(double) angleForTouchPoint:(CGPoint) point
{
    // We use the positive counterclockwise coordinate system in the drawing code since that's what's used traditionally
    //    outside of Apple's APIs, so multiplying the result of
    //    atan2() by -1 converts the angle from a positive clockwise unit circle to a positive counterclockwise
    //    unit circle.

    double angleInRadians = -1*atan2(point.y - (self.frame.size.height/2), point.x - self.frame.size.width/2);

    if (angleInRadians < 0) // Convert angle to 0 - 2pi radians; we want non-negative angles.
    {
        angleInRadians += M_PI*2;
    }

    return angleInRadians;
}
// points get fed into this from the UIView.
-(void) updateWithTouchAtPoint:(CGPoint) point
{
    if ([self checkSliderTouchAtPoint:point])
    {
        double touchAngle = [self angleForTouchPoint:point];
        _slider.transform = CATransform3DMakeRotation(-M_PI, 0.0, 0.0, 1.0); // Hardcoded angle rotation for now since I need to do some subtraction later in order to determine the amount to rotate based on touches.
    }
}

I really appreciate any help that you can provide!

Was it helpful?

Solution

I would generally expect that event locations are expressed in terms of the UIView they're being delivered to. Since there's a transform on _slider with respect to it's superlayer (i.e. the UIView's backing layer), any geometric value you want to use will need to be transformed into that frame of reference. In short, you need to explicitly convert the point into _slider's frame of reference. Try something like this:

-(BOOL) checkSliderTouchAtPoint: (CGPoint) point
{
    CGPoint pointInSliderLayer = [_slider convertPoint: point fromLayer: self.layer];
    return CGPathContainsPoint(_slider.path , NULL, pointInSliderLayer, NO);
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top