Question

I'm trying to make a circle-masked image view with animated mask, and played with different solutions. The example below does the job but I've got two problems with it:

1) Why is it that I cannot make the image tappable? Adding eg. a UITapGestureRecognizer does not work. My guess is that the mask prevents the touch actions being propagated to the lower level in the view hierarchy.

2) Animating the mask is running very fast and I cannot adjust the duration using UIView block animation

How can I solve these?

- (void) addCircle {
    // this is the encapsulating view
    //
    base = [[UIView alloc] init];
    //
    // this is the button background
    //
    base_bgr = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"c1_bgr.png"]];
    base_bgr.center = CGPointMake(60, 140);
    [base addSubview:base_bgr];
    //
    // icon image
    //
    base_icon = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"c1_ico.png"]];
    base_icon.center = CGPointMake(186*0.3/2, 182*0.3/2);
    base_icon.transform = CGAffineTransformMakeScale(0.3, 0.3);
    [base addSubview:base_icon];
    //
    // the drawn circle mask layer
    //
    circleLayer = [CAShapeLayer layer];
    // Give the layer the same bounds as your image view
    [circleLayer setBounds:CGRectMake(0.0f, 0.0f, [base_icon frame].size.width,
                                      [base_icon frame].size.height)];
    // Position the circle
    [circleLayer setPosition:CGPointMake(186*0.3/2-7, 182*0.3/2-10)];
    // Create a circle path.
    UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:
                          CGRectMake(0.0f, 0.0f, 70.0f, 70.0f)];
    // Set the path on the layer
    [circleLayer setPath:[path CGPath]];

    [[base layer] setMask:circleLayer];

    [self.view addSubview:base];
    base.center = CGPointMake(100, 100);
    base.userInteractionEnabled = YES;
    base_bgr.userInteractionEnabled = YES;
    base_icon.userInteractionEnabled = YES;

    //
    // NOT working: UITapGestureRecognizer
    //
    UITapGestureRecognizer *tapgesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapit:)];
    [base_icon addGestureRecognizer:tapgesture];

    //
    // BAD but WORKS :) properly positioned UIButton over the masked image
    //
    base_btn = [UIButton buttonWithType:UIButtonTypeCustom];
    base_btn.frame = CGRectMake(base.frame.origin.x, base.frame.origin.y, base_icon.frame.size.width, base_icon.frame.size.height);
    [base_btn addTarget:self action:@selector(tapit:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:base_btn];
}

This is the tap handler, and here's the mask animation. Whatever number I tried in duration, it is animating fast - approximately 0.25 second, and I cannot adjust it.

- (void) tapit:(id) sender {
    //...
        [UIView animateWithDuration:1.0
                              delay:0.0
                            options:UIViewAnimationOptionCurveEaseOut
                         animations:^ {
                             [circleLayer setTransform:CATransform3DMakeScale(10.0, 10.0, 1.0)];
                         }
                         completion:^(BOOL finished) {
                             // it is not necessary if I manage to make the icon image tappable
                             base_btn.frame = [base convertRect:base_icon.frame toView:self.view];  
                         }];
    }    
}
Was it helpful?

Solution

1) touches are not propagated down from base because it's initiated without a frame, so its frame will be CGRectZero. Views don't get touch events that start outside of their bounds. Simply set a valid frame on base that includes entire tap target.

2) setTransform: on a layer invokes an implicit animation which uses Core Animation's default duration of 0.25 (you guessed it right :)). The best solution would be to use CABasicAnimation instead of UIView-based animation. Something like this:

CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
scaleAnimation.toValue = @(10.0f);
scaleAnimation.duration = 2;
[circleLayer addAnimation:scaleAnimation forKey:nil];

Note: by default, CABasicAnimation will remove itself from a layer when complete and the layer will snap back to old values. You can prevent it by for example setting that animation's removedOnCompletion property to NO and removing it yourself later using CALayer's removeAnimationForKey: method (just set a key instead of passing nil wheen adding the animation), but that depends on what exactly you want to accomplish with this.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top