UIView animation: moving along an arbitrary path using an example of a circle

Published on March 06, 2013

UIView animation: moving along an arbitrary path using an example of a circle

Perhaps most iOs developers know that a few lines of code are usually enough to implement various visual effects. The UIKit framework, which is responsible for the standard interface, has built-in tools that allow you to make quite sophisticated types of animation - from moving in a straight line, to the effect of turning the page. However, to move the heirs of UIView along a more complex trajectory, you have to go down below and go to the level of the Core Graphics framework. In this case, the number of examples on the network is reduced and it is difficult to find the necessary. And if it is, then the quality of implementation often leaves much to be desired. I faced this situation when it became necessary to make an animation of an interactive book for children.


Animation engine



To implement the movement along an arbitrary trajectory, the following approach is used:

  1. a path is constructed consisting of figures (straight lines, curves, circles, etc.). For this, the CGPath structure and auxiliary functions for working with it are used. By the way, this structure can also be used to draw the resulting figure.
  2. A CAKeyframeAnimation animation is created that describes the behavior - duration, type of approximation, time offset, etc. The path created earlier also “clings” to this object.
  3. The CGLayer object is given the command to execute the resulting animation.


Building a path


There are two types of paths: static CGPathRef and mutable CGMutablePathRef. The first one is created using one of the functions, after creation it cannot be changed. For example, CGPathCreateWithEllipseInRect (CGRect rect, const CGAffineTransform * transform) creates an ellipse inscribed in a rectangle from the first parameter and superimposes the transformation matrix from the second parameter on it. This is the easiest and fastest way to create a path, but it has a drawback - the beginning of such a path will be between the 1st and 4th quarters, at 0 (360) degrees and have an hourly direction. If we just want to draw the resulting path, this approach may well come in handy. But in the case of animation, it will be inconvenient - the beginning and direction matter.

The second type of path, CGMutablePathRef, is created either empty and complemented by separate functions, or by creating a mutable copy of an existing path. For example, consider creating a circle centered at an arbitrary point:

CGPoint center = CGPointMake(200.0, 200.0);
CGFloat radius = 100.0;
CGMutablePathRef path = CGPathCreateMutable(); 
CGPathAddArc(path, NULL, center.x, center.y, radius, M_PI, 0, NO);       //А
CGPathAddArc(path, NULL, center.x, center.y, radius, 0, M_PI, NO);
CGPathRelease(path);                                                     //Б


  • The CGPathAddArc function adds an arc to the path and accepts the following parameters:
    1. mutable path
    2. transformation matrix
    3. X coordinate of the center of the circle
    4. The coordinate of the center of the circle
    5. arc radius
    6. angle from the X axis to the beginning of the arc, in radians
    7. angle to the end of the arc
    8. direction, in this case counterclockwise

  • The responsibility for releasing the created resource lies with the programmer. Programmer, remember: leakage is bad. The application will consume memory, Apple will be indignant, and the user will be upset.


The value of some parameters of the CGPathAddArc function may not be obvious and for a better understanding we look at the picture below:



A is the center of the imaginary circle along which our arc will lie. The coordinates specify parameters 3 and 4.
B - the beginning of the arc, is given by the angle, parameter 6. C
- the end of the arc, similarly, parameter 7.

Create and run animations


Everything is simpler here:

CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
pathAnimation.path = path;
pathAnimation.duration = 2.0f;
 [view.layer addAnimation:pathAnimation forKey:nil];


We create an instance of CAKeyframeAnimation and pass the Key-Value path constructor to the property that we want to animate. In our case, this is “position”.
Assign animations to the previously created CGPathRef.
Set the duration of the animation.
We take the UIView we need, find its CGLayer and call the animation to play.

Everything, after that, the animation will begin to play. The second parameter is nil and our animation will remain unnamed. It will not be possible to contact her, but so far this is not required for us.
Everything seems to be simple, but there is a nuance. How to combine the beginning of the path with UIView? After all, if this is not done, the picture at the beginning of the animation will simply jump to the beginning of the first arc. In order for everything to work as it should, it will have to be complicated - which is what we will do next.

From theory to practice



In the above example, everything is simple and good, but boring and clumsy. To make it more fun, we’ll write a small application in which the picture will move in an arc to the specified point. Here is a video of what should be the result:



First, create a Single View project and add the QuartzCore framework to it. Then change the title of the ViewController:

@class PathDrawingView; // 1
@interface CMViewController : UIViewController
{
    UIImageView     *_image;	//2
    BOOL            _isAnimating;	//3
    BOOL            _drawPath;	//4
}
@property (retain, nonatomic) PathDrawingView *pathView; //5
@end


  1. We declare a helper class that will be responsible for rendering our path. This greatly facilitates debugging.
  2. A simple picture that we will move.
  3. Flag play animation.
  4. Flag for drawing the path, if we suddenly want to see how our picture will move.
  5. The assistant’s mechanics involve multiple creation and deletion. Declare it as a property to simplify this process.


Now to the implementation. And let's start from the beginning, that is, by adding the necessary headers and declaring a constant:

#import <QuartzCore/QuartzCore.h>
#import "PathDrawingView.h"
static NSString *cAnimationKey = @"pathAnimation";


With the first heading it’s clear, and the second is a helper class. The constant is useful to us for naming animations.

Now change the viewDidLoad method:

- (void) viewDidLoad
{
    [super viewDidLoad];
    _drawPath = NO;
    _isAnimating = NO;
    _image = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"image.png"]];
    _image.center = CGPointMake(160, 240);
    [self.view addSubview:_image];
}


Set the flags. If you suddenly want to see what our path looks like, you will need to activate _drawPath. It’s clear that _isAnimating is not installed yet - the animation is not playing yet. Next, create an image and show it.

It is necessary to create a path, select it in a separate method:

- (CGPathRef) pathToPoint:(CGPoint) point
{
    CGPoint imagePos = _image.center;
    CGFloat xDist = (point.x - imagePos.x);
    CGFloat yDist = (point.y - imagePos.y);
    CGFloat radius = sqrt((xDist * xDist) + (yDist * yDist)) / 2;	// 1
    CGPoint center = CGPointMake(imagePos.x + radius, imagePos.y); //2
    CGFloat angle = atan2f(yDist, xDist);		// 3
    CGAffineTransform transform = CGAffineTransformIdentity;
    transform = CGAffineTransformTranslate(transform, imagePos.x, imagePos.y);
    transform = CGAffineTransformRotate(transform, angle);
    transform = CGAffineTransformTranslate(transform, -imagePos.x, -imagePos.y);	//4
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddArc(path, &transform, center.x, center.y, radius, M_PI, 0, YES);
    //CGPathAddArc(path, &transform, center.x, center.y, radius, 0, M_PI, YES);		//5
    return path;
}


The destination point (hereinafter referred to as T) is passed to the method and it is conditionally divided into 4 blocks:

  1. By the Pythagorean theorem, we calculate the distance between the picture and T. Divide by two and get the radius of the arc, the beginning of which will be in the picture, and the end - at the desired point.
  2. First, we will work in the coordinate system, where the center of the picture and T are on the same line passing along the Y axis. In this coordinate system, the center of the desired circle will be shifted by the distance of the radius along the X axis.
  3. We find the angle between the center of the picture and T. Of course, in the original coordinate system. To do this, we use the previously found vector from T to the center of the picture.
  4. Create a rotation matrix for the transition from an arbitrary coordinate system to the "real" one.
  5. Create a path. At this point, we have all the necessary data. Note that one line is commented out. Only one arc is created - we want the picture to stop at the specified point, and not go through it and come back.


Let's move on to the animation itself:

- (void) followThePath:(CGPathRef) path
{
    CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
    pathAnimation.path = path;
    pathAnimation.removedOnCompletion = NO;  	// 1
    pathAnimation.fillMode = kCAFillModeForwards;	//2
    pathAnimation.duration = 2.0f;
    pathAnimation.calculationMode = kCAAnimationPaced;	//3
    pathAnimation.delegate = self; //4
    [_image.layer addAnimation:pathAnimation forKey:cAnimationKey]; 		//5
}


What's new?

  1. Indicates that the animation should remain after completion. This is necessary so that we can read the last value. But why this is needed will be clear later.
  2. Indicates that the animation object (i.e. the picture we will be moving) should remain in the state in which the animation ended. If removed, the picture will jump to where the movement began.
  3. Sets the method for calculating intermediate frames of animation. If you want (and we want!) To stop the animation at any time, you need to specify just such a view. Otherwise, the picture will jump, and not stop exactly in the current position.
  4. We designate ourselves as an animation delegate to catch the moment of its end.
  5. We start the animation. This time, give her a name.


Now we need to process the end of the animation:

- (void) stop
{
    CALayer *pLayer = _image.layer.presentationLayer;		// 1
    CGPoint currentPos = pLayer.position;
    [_image.layer removeAnimationForKey:cAnimationKey];	// 2
    [_image setCenter:currentPos];
    _isAnimating = NO;
}


  1. We take the presentation layer, it is there that the animation is spinning and contains up-to-date information about the state of the object during its playback - this is a feature of the Core Graphics framework. If this is not done, then the picture will jump to where the animation started.
  2. We remove our animation.


Add an animation delegate method:

- (void) animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
    if (flag)
        [self stop];
}


Everything is simple here: if the animation ended by itself, we stop it and do the necessary actions. In case of forced interruption, we will stop it in another place. Here, in the touch handler:

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (_isAnimating)
        [self stop];
    _isAnimating = YES;
    UITouch *touch = [touches anyObject];
    CGPoint touchPoint = [touch locationInView:self.view];
    CGPathRef path = [self pathToPoint:touchPoint];
    [self followThePath:path];
    if (_drawPath)
        [self drawPath:path];
    CGPathRelease(path);
}


Here we simply combine everything written earlier and free the created path.
It remains to add a debugging method to draw the path:

- (void) drawPath:(CGPathRef) path
{
    [self.pathView removeFromSuperview];			// 1
    self.pathView = [[PathDrawingView alloc] init];		// 2
    self.pathView.path = path;
    self.pathView.frame = self.view.frame;
    [self.view addSubview:self.pathView];
}


  1. We remove the previous path from the screen, otherwise there will be porridge
  2. Create a special object to draw the path. Its code will be below.


Finally, free up resources:

- (void) viewDidUnload
{
    [_image release];
    self.pathView = nil;
}


That's all, now you can run.

application


PathDrawingView.h
#import <UIKit/UIKit.h>
@interface PathDrawingView : UIView
{
    CGPathRef   _path;
}
@property (retain, nonatomic) UIColor *strokeColor;
@property (retain, nonatomic) UIColor *fillColor;
@property (assign, nonatomic) CGPathRef path;
@end

PathDrawingView.m
#import "PathDrawingView.h"
#import <QuartzCore/QuartzCore.h>
@implementation PathDrawingView
@synthesize strokeColor, fillColor;
- (CGPathRef) path
{
    return _path;
}
- (void) setPath:(CGPathRef)path
{
    CGPathRelease(_path);
    _path = CGPathRetain(path);
}
- (void)drawRect:(CGRect)rect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextSetStrokeColorWithColor(ctx, strokeColor.CGColor);
    CGContextSetFillColorWithColor(ctx, fillColor.CGColor);
    CGContextAddPath(ctx, _path);
    CGContextStrokePath(ctx);
}
- (id) init
{
    if (self = [super init])
    {
        self.fillColor = [UIColor clearColor];
        self.strokeColor = [UIColor redColor];
        self.backgroundColor = [UIColor clearColor];
    }
    return self;
}
- (void) dealloc
{
    self.fillColor = nil;
    self.strokeColor = nil;
    CGPathRelease(_path);
    [super dealloc];
}
@end


GitHub Project Code
Core Animation Programming Guide - Description of the intricacies of the framework.
CGPathRef reference - As well as functions for working with this structure.