Digitech
Digitech

Reputation: 292

How to change direction of CALayer animation

How to change direction and start position of CALayer animation?

For example, if I animate a circle shape using following code:

-(void)drawCircle {
    CAShapeLayer *circleShapeLayer = [[CAShapeLayer alloc] init];
    circleShapeLayer.path          = [self circleBezierPath].CGPath;
    circleShapeLayer.strokeColor   = [UIColor lightGrayColor].CGColor;
    circleShapeLayer.fillColor     = [UIColor clearColor].CGColor;
    circleShapeLayer.lineWidth     = 2;
    [self.view.layer addSublayer:circleShapeLayer];

    CABasicAnimation *circleShapeAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
    circleShapeAnimation.timingFunction    = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    circleShapeAnimation.duration          = 1.0f;
    circleShapeAnimation.fromValue         = @(0.0f);
    circleShapeAnimation.toValue           = @(1.0f);
    [circleShapeLayer addAnimation:circleShapeAnimation forKey:@"strokeEnd"];
}

-(UIBezierPath *)circleBezierPath {
    UIBezierPath *circlePath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(100, 100, 100, 100)];
    return circlePath;
}

Animation always starts at 3 o'clock and animates in a clockwise direction.

How do I force it to start at 12 o'clock (or any other position) and animate in a counterclockwise direction?

Upvotes: 1

Views: 1428

Answers (1)

David Rönnqvist
David Rönnqvist

Reputation: 56625

What is happening

When you create an oval in a rectangle (ether using UIBezierPath of the CGPath functions) it creates an oval or ellipse that fits the specified rectangle. This means that the center of the oval is not the same as the origin of the rectangle:

An oval in a rectangle

The animation always starts at 3 o'clock and goes counter clockwise because that is the arcing path that is a circle from 0 to 2π in the default coordinate system:

Angles in the default coordinate system

This means that the path you are animating the stroke of really looks like this:

enter image description here

The wrong way to solve the problem

Looking at the path, you could move the start point by rotating the view. This is not recommended because it has side effects. If there are any sublayers, those will be rotated as well. The stroke direction could be changed either by flipping the view (scaling negative along one axis) or by animating the start of the stroke from 1 to 0 instead of the end of the stroke.

All of theses solutions will work, but has side effects and is less clear that what is preferable.

A better solution

I like to look at the problem like this: the animation code is doing the right thing, animating the stroke from nothing to all of it, but the start point and stroke direction of the path is not what's expected. This means that the "problem" lies in the path.

Instead of using bezierPathWithOvalInRect:, you can create your own full circle using an arc that goes from any start angle to any end angle in either a clockwise or counter clockwise direction. All that's required is that you calculate the center of the circle (and the start and end angles). Looking at the default coordinate system, we can see that the start angle is 3π/2 or -π/2. The end angle is then plus or minus 2π from the start angle (depending on if it's clockwise or counter clockwise).

Creating an arc like this could look something like this:

- (UIBezierPath *)circleBezierPath
{
    CGRect boundingRect = CGRectMake(100, 100, 100, 100);
    CGPoint center      = CGPointMake(CGRectGetMidX(boundingRect), CGRectGetMidY(boundingRect));
    CGFloat radius      = MIN(CGRectGetWidth(boundingRect), CGRectGetHeight(boundingRect));

    CGFloat twelveOClockAngle = -M_PI_2;
    BOOL clockwise = NO; // NO for counter clockwise

    CGFloat startAngle = twelveOClockAngle;
    CGFloat endAngle = startAngle + (2.0*M_PI * (clockwise ? 1.0 : -1.0));

    return [UIBezierPath bezierPathWithArcCenter:center
                                          radius:radius
                                      startAngle:startAngle
                                        endAngle:endAngle
                                       clockwise:clockwise];
}

Upvotes: 11

Related Questions