Search code examples
objective-ccore-animationcalayer

Circular-slider weird behaviour while animating with CA


I wanted to make a circular Slider which is draggable and animatable. So far I've managed to build the slider and use the drag handle to move it around and even animate it. Sometimes animation goes wrong (wrong direction or shortest direction. I've subclassed a UIView (Will be a UIControl soon, just wanted to get the animation right first) added a PanGestureRecognizer and several layers for the drawing.

enter image description here

So how do I fix this weird behaviour? I've someone could help me here, I'd be thankful :)

Here's the sample project -> http://cl.ly/2l0O3b1I3U0X

Thanks a lot!

EDIT:

Here's the drawing code:

CALayer *aLayer = [CALayer layer];
aLayer.bounds = CGRectMake(0, 0, 170, 170);
aLayer.position = self.center;
aLayer.transform = CATransform3DMakeRotation(M_PI_4, 0, 0, 1);

self.handleHostLayer = [CALayer layer];
self.handleHostLayer.bounds = CGRectMake(0, 0, 170, 170);
self.handleHostLayer.position = CGPointMake(CGRectGetMaxX(aLayer.bounds) - 170/2.0, CGRectGetMaxY(aLayer.bounds) - 170/2.0);

[aLayer addSublayer:self.handleHostLayer];
[self.layer addSublayer:aLayer];

self.handle = [CALayer layer];
self.handle.bounds = CGRectMake(0, 0, 50, 50);
self.handle.cornerRadius = 25;
self.handle.backgroundColor = [UIColor whiteColor].CGColor;
self.handle.masksToBounds = NO;
self.handle.shadowOffset = CGSizeMake(3.0, 0.0);
self.handle.shadowRadius = 0;
self.handle.shadowOpacity = .15;
self.handle.shadowColor = [UIColor blackColor].CGColor;

[self.handleHostLayer addSublayer:self.self.handle];

Here's the animation code:

CGFloat handleTarget = ToRad(DEG);

CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
rotationAnimation.fromValue = @([[self.handleHostLayer valueForKeyPath:@"transform.rotation"] floatValue]);
rotationAnimation.toValue = @(handleTarget);
rotationAnimation.duration = .5;
rotationAnimation.removedOnCompletion = NO;
rotationAnimation.fillMode = kCAFillModeForwards;
rotationAnimation.cumulative = YES;
rotationAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];

[self.handleHostLayer addAnimation:rotationAnimation forKey:@"transform.rotation"];

Solution

  • OK, I looked at your project. Your problem is that the to and from angles don't both fall in the 0≤ɸ<2π range.

    You can make sure that they do by adding and removing 2π until they both are within that range.

    CGFloat fromAngle = [[self.handleHostLayer valueForKeyPath:@"transform.rotation"] floatValue];
    CGFloat toAngle   = handleTarget;
    
    while (fromAngle >= 2.0*M_PI) { fromAngle -= 2*M_PI; }
    while (toAngle   >= 2.0*M_PI) { toAngle   -= 2*M_PI; }
    while (fromAngle <  0.0)      { fromAngle += 2*M_PI; }
    while (toAngle   <  0.0)      { toAngle   += 2*M_PI; }
    
    CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
    rotationAnimation.fromValue = @(fromAngle);
    rotationAnimation.toValue   = @(toAngle);
    // As before...
    

    Another things you could change is the misuse of removeOnCompletion. I did a long explanation in this answer about why it's bad to do so.

    In short: you are not animating from the value that you think you are animating since you inadvertently introduce a difference between the value of the layers property and what you see on screen.

    Skip that line all together. You can also skip the skip the cumulative line since you are not repeating the animation. Now, if your animations doesn't stick: set the model value to its final value before adding the animation to it.