📜 ⬆️ ⬇️

Animation UIView: moving along an arbitrary trajectory on the example of a circle

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


Animation mechanism



To implement movement along an arbitrary trajectory, the following approach is used:
')
  1. a path is constructed consisting of figures (straight lines, curves, circles, etc.). This is done using the CGPath structure and auxiliary functions for working with it. By the way, this structure can also be used to draw the resulting shape.
  2. A CAKeyframeAnimation animation is created that describes the behavior — duration, type of approximation, time offset, etc. The previously created path 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 variable CGMutablePathRef. The first 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 imposes on it a transformation matrix from the second parameter. 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 the direction matters.

The second type of path, CGMutablePathRef, is created either empty and supplemented with separate functions, or by creating a modifiable copy of an existing path. For example, consider creating a circle with a center 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 value of some parameters of the CGPathAddArc function may not be obvious, and for a better understanding, look at the picture below:



A is the center of an imaginary circle along which our arc will run. Coordinates set parameters 3 and 4.
B - the beginning of the arc, given by the angle, parameter 6.
In - the end of the arc, similarly, parameter 7.

Creating and running animation


It's all easier:

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


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

Everything after this animation will start playing. The second parameter is nil and our animation will remain nameless. It will be impossible to contact her, but so far we don’t need it.
It seems to be all 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, we will have to complicate - what we will do next.

From theory to practice



In the example above, everything is simple and good, but boring and clumsy. To make it more fun, we will write a small application in which the picture will move along 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 ViewController header:

 @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 makes debugging much easier.
  2. A simple picture that we will move.
  3. Flag playing animation.
  4. A flag to draw the path if we suddenly want to see how our picture will move.
  5. The mechanics of the work of an assistant imply multiple creation and deletion. We 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"; 


The first heading is understandable, and the second is the class assistant. The constant is useful to us for naming animations.

Now we 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 we suddenly want to see how our path looks, we will need to activate _drawPath. It is clear that _isAnimating is not yet set for us - the animation is not playing yet. Next, create an image and display it.

We need 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 method passes the destination point (T) and it is conventionally divided into 4 blocks:

  1. By the Pythagorean theorem, we calculate the distance between the picture and T. Divide into 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 image and T are on one straight line, passing along the Y axis. In this coordinate system, the center of the desired circle will be shifted by a distance of radius along the X axis.
  3. Find the angle between the center of the picture and T. Of course, in the original coordinate system. To do this, use the previously found vector from T to the center of the image.
  4. Create a rotation matrix for the transition from an arbitrary coordinate system to the "real".
  5. Create a path. By this moment we have all the necessary data. Please note that one line is commented out. Only one arc is created - we want the picture to stop at the specified point, rather than pass 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 here?

  1. Indicates that the animation should remain after the end. This is necessary so that we can read the last value. But why you need it - it will be clear later.
  2. Indicates that the animation object (i.e., the image that we are going to move) should remain in the state in which the animation ended. If you remove, the picture will jump to where it started moving.
  3. Sets the method for calculating animation intermediate frames. If we want (and we want!) To stop the animation at an arbitrary moment, we need to specify just such a view. Otherwise, the picture will jump, and not stop at exactly the current position.
  4. We assign ourselves as the delegate of animation to catch the moment of its termination.
  5. Run the animation. This time, we give her a name.


Now we need to handle 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 actual information about the state of the object during its playback - this is a feature of the Core Graphics framework. If this is not done, the picture will jump to where the animation began.
  2. We remove our animation.


Add the delegate delegate method:

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


Everything is simple here: if the animation is over itself, we stop it and do the necessary actions. In the case of a forced interruption, stop it in another place. Right 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 connect everything written earlier and release the created path.
It remains to add a debugging method for drawing 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. Remove the previous path from the screen, otherwise there will be porridge
  2. Create a special object for drawing the path. His code will be lower.


Finally, free up resources:

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


That's it, 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 - A description of the subtleties of the framework.
CGPathRef reference - And also, functions for working with this structure.

Source: https://habr.com/ru/post/171735/


All Articles