Пожалуй, большинство iOs разработчиков знают, что для реализации различных визуальных эффектов, обычно, достаточно нескольких строчек кода. Фреймворк UIKit, отвечающий за стандартный интерфейс, имеет встроенные средства, позволяющие делать довольно изощрённые виды анимации — от перемещения по прямой, до эффекта переворачивания страницы. Однако, для перемещения наследников UIView по более сложной траектории, приходится спускаться ниже и переходить на уровень фреймворка Core Graphics. При этом, количество примеров в сети снижается и бывает сложно найти необходимое. А если и находится, то качество реализации, зачастую, оставляет желать лучшего. С такой ситуацией я и столкнулся, когда возникла необходимость сделать анимацию интерактивной книги для детей.
Для реализации движения по произвольной траектории используется следующий подход:
Пути бывают двух типов: статичный CGPathRef и изменяемый CGMutablePathRef. Первый создаётся с помощью одной из функций, после создания изменить его нельзя. Например, CGPathCreateWithEllipseInRect( CGRect rect, const CGAffineTransform *transform) создаёт эллипс, вписаный в прямоугольник из первого параметра и накладывает на него матрицу трансформации из второго параметра. Это самый простой и быстрый способ создать путь, но у него есть недостаток – начало такого пути будет находится между 1-й и 4-й четвертями, в 0 (360) градусах и иметь почасовое направление. Если мы хотим просто отрисовать полученый путь, такой подход вполне может пригодиться. Но в случае с анимацией, это будет неудобно – начало и направление имеет значение.
Второй тип путей, CGMutablePathRef, создаётся либо пустым и дополняется отдельными функциями, либо с помощью создания изменяемой копии существующего пути. Для примера, рассмотрим создание окружности с центром в произвольной точке:
Значение некоторых параметров функции CGPathAddArc может быть не очевидно и для лучшего понимания посмотрим на приведённую ниже картинку:

А – центр воображаемой окружности, по которой будет пролегать наша дуга. Координаты задают параметры 3 и 4.
Б – начало дуги, задаётся углом, параметр 6.
В – конец дуги, аналогично, параметр 7.
Тут всё проще:
Создаём экземпляр CAKeyframeAnimation и передаём конструктору Key-Value path до свойства, которое хотим анимировать. В нашем случае это „position”.
Присваиваем анимации ранее созданый CGPathRef.
Устанавливаем длительность анимации.
Берём нужный нам UIView, находим его CGLayer и вызываем проигрывание анимации.
Всё, после этого анимация начнёт проигрываться. Вторым параметром передаётся nil и наша анимация останется безимянной. К ней невозможно будет обращаться, но нам пока это и не требуется.
Вроде бы всё просто, но есть ньюанс. Как совместить начало пути с UIView? Ведь если этого не сделать, картинка при начале анимации будет просто перепрыгивать в начало первой дуги. Для того, чтобы всё работало как надо, придётся усложнять – чем мы и займёмся дальше.
В вышепреведённом примере всё просто и хорошо, но скучно и коряво. Чтобы было веселее, напишем небольшое приложение, в котором картинка будет двигаться по дуге к указанной точке. Вот ролик того, что должно получиться в итоге:
Для начала, создаём Single View проект и добавляем в него QuartzCore framework. Затем меняем заголовок ViewController:
Теперь к реализации. И начнём с начала, то есть, с добавления нужных заголовков и объявления константы:
С первым заголовком понятно, а второй это класс-помошник. Константа нам пригодится для именования анимации.
Теперь меняем метод viewDidLoad:
Устанавливаем флаги. Если вдруг захотим посмотреть как выглядит наш путь, надо будет активировать _drawPath. Понятно, _isAnimating у нас пока не установлен – анимация ещё не проигрывается. Далее, создаём изображение и показываем его.
Надо создать путь, выделим это в отдельный метод:
Методу передаётся точка назначения (далее Т) и он условно разбит на 4 блока:
Перейдём к самой анимации:
Что тут нового?
Теперь надо обработать окончание анимации:
Добавляем метод делегата анимации:
Здесь всё просто: если анимация закончилась сама, мы её останавливаем и делаем необходимые действия. В случае принудительного прерывания, остановим её в другом месте. Вот тут, в обработчике касания:
Тут мы просто соединяем всё написаное ранее и освобождаем созданый путь.
Осталось добавить отладочный метод для отрисовки пути:
Наконец, освобождаем ресурсы:
Вот и всё, теперь можно запускать.
GitHub Код проекта
Core Animation Programming Guide — Описание тонкостей работы фреймворка.
CGPathRef reference — А также, функций для работы с этой структурой.
Механизм анимации
Для реализации движения по произвольной траектории используется следующий подход:
- строится путь, состоящий из фигур (прямые, кривые, окружности и прочее). Для этого используется структура CGPath и вспомогательные функуции для работы с ней. Кстати, эту структуру можно использовать и для отрисовки полученой фигуры.
- Создаётся анимация CAKeyframeAnimation, которая описывает поведение — длительность, тип аппроксимации, смещение по времени и т.д. К этому объекту также “цепляется” созданый ранее путь.
- Объекту CGLayer отдаётся команда выполнить полученую анимацию.
Построение пути
Пути бывают двух типов: статичный CGPathRef и изменяемый CGMutablePathRef. Первый создаётся с помощью одной из функций, после создания изменить его нельзя. Например, CGPathCreateWithEllipseInRect( CGRect rect, const CGAffineTransform *transform) создаёт эллипс, вписаный в прямоугольник из первого параметра и накладывает на него матрицу трансформации из второго параметра. Это самый простой и быстрый способ создать путь, но у него есть недостаток – начало такого пути будет находится между 1-й и 4-й четвертями, в 0 (360) градусах и иметь почасовое направление. Если мы хотим просто отрисовать полученый путь, такой подход вполне может пригодиться. Но в случае с анимацией, это будет неудобно – начало и направление имеет значение.
Второй тип путей, CGMutablePathRef, создаётся либо пустым и дополняется отдельными функциями, либо с помощью создания изменяемой копии существующего пути. Для примера, рассмотрим создание окружности с центром в произвольной точке:
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); //Б
- Функция CGPathAddArc добавляет дугу к пути и принимает следующие параметры:
- изменяемый путь
- матрица трансформации
- Х координата центра окружности
- У координата центра окружности
- радиус дуги
- угол от оси Х к началу дуги, в радианах
- угол к концу дуги
- направление, в данном случае против часовой стрелки
- Ответственность за освобождение созданного ресурса лежит на программисте. Программист, помни: утечки это плохо. Приложение будет жрать память, эппл – негодовать, а пользователь – расстраиваться.
Значение некоторых параметров функции CGPathAddArc может быть не очевидно и для лучшего понимания посмотрим на приведённую ниже картинку:

А – центр воображаемой окружности, по которой будет пролегать наша дуга. Координаты задают параметры 3 и 4.
Б – начало дуги, задаётся углом, параметр 6.
В – конец дуги, аналогично, параметр 7.
Создание и запуск анимации
Тут всё проще:
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"]; pathAnimation.path = path; pathAnimation.duration = 2.0f; [view.layer addAnimation:pathAnimation forKey:nil];
Создаём экземпляр CAKeyframeAnimation и передаём конструктору Key-Value path до свойства, которое хотим анимировать. В нашем случае это „position”.
Присваиваем анимации ранее созданый CGPathRef.
Устанавливаем длительность анимации.
Берём нужный нам UIView, находим его CGLayer и вызываем проигрывание анимации.
Всё, после этого анимация начнёт проигрываться. Вторым параметром передаётся nil и наша анимация останется безимянной. К ней невозможно будет обращаться, но нам пока это и не требуется.
Вроде бы всё просто, но есть ньюанс. Как совместить начало пути с UIView? Ведь если этого не сделать, картинка при начале анимации будет просто перепрыгивать в начало первой дуги. Для того, чтобы всё работало как надо, придётся усложнять – чем мы и займёмся дальше.
От теории к практике
В вышепреведённом примере всё просто и хорошо, но скучно и коряво. Чтобы было веселее, напишем небольшое приложение, в котором картинка будет двигаться по дуге к указанной точке. Вот ролик того, что должно получиться в итоге:
Для начала, создаём Single View проект и добавляем в него QuartzCore framework. Затем меняем заголовок ViewController:
@class PathDrawingView; // 1 @interface CMViewController : UIViewController { UIImageView *_image; //2 BOOL _isAnimating; //3 BOOL _drawPath; //4 } @property (retain, nonatomic) PathDrawingView *pathView; //5 @end
- Объявляем класс-помошник, который будет отвечать за отрисовку нашего пути. Это сильно облегчает отладку.
- Простая картинка, которую мы будет двигать.
- Флаг проигрывания анимации.
- Флаг для отрисовки пути, если вдруг захотим посмотреть как будет двигаться наша картинка.
- Механика работы помошника подразумевает многократное создание и удаление. Объявим его как свойство, для упрощения этого процесса.
Теперь к реализации. И начнём с начала, то есть, с добавления нужных заголовков и объявления константы:
#import <QuartzCore/QuartzCore.h> #import "PathDrawingView.h" static NSString *cAnimationKey = @"pathAnimation";
С первым заголовком понятно, а второй это класс-помошник. Константа нам пригодится для именования анимации.
Теперь меняем метод viewDidLoad:
- (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]; }
Устанавливаем флаги. Если вдруг захотим посмотреть как выглядит наш путь, надо будет активировать _drawPath. Понятно, _isAnimating у нас пока не установлен – анимация ещё не проигрывается. Далее, создаём изображение и показываем его.
Надо создать путь, выделим это в отдельный метод:
- (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; }
Методу передаётся точка назначения (далее Т) и он условно разбит на 4 блока:
- По теореме Пифагора вычисляем расстояние между картинкой и Т. Делим на два и получаем радиус дуги, начало которой будет в картинке, а конец – в нужной точке.
- Сначала будем работать в системе координат, где центр картинки и Т находятся на одной прямой, проходящей по оси Y. В этой системе координат, центр искомой окружности будет смещён на расстояние радиуса по оси X.
- Находим угол между центром картинки и Т. Конечно, в исходной системе координат. Для этого используем найденый ранее вектор от Т к центру картинки.
- Создаём матрицу поворота для перехода из произвольной системы координат к «настоящей».
- Создаём путь. К этому моменту у нас есть все необходимые данные. Обратите внимание, что одна строчка закоментирована. Создаётся только одна дуга – мы хотим чтобы картинка остановилась в указаной точке, а не прошла через неё и вернулась обратно.
Перейдём к самой анимации:
- (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 }
Что тут нового?
- Указывает, что анимация должна остаться после окончания. Это нужно, чтобы мы могли прочитать последнее значение. А вот зачем нужно это – будет понятно позже.
- Указывает, что объект анимации (т.е. картинка, которую мы будем двигать) должна оставаться в том состоянии, в котором закончилась анимация. Если убрать, картинка будет перепрыгивать туда, откуда начинала движение.
- Устанавливает способ расчёта промежуточных кадров анимации. Если хотим (а мы хотим!) останавливать анимацию в произвольный момент, надо указывать именно такой вид. В противном случае, картинка будет прыгать, а не останавливаться точно в текущем положении.
- Назначаем себя делегатом анимации, чтобы ловить момент её окончания.
- Запускаем анимацию. На этот раз, присваеваем ей имя.
Теперь надо обработать окончание анимации:
- (void) stop { CALayer *pLayer = _image.layer.presentationLayer; // 1 CGPoint currentPos = pLayer.position; [_image.layer removeAnimationForKey:cAnimationKey]; // 2 [_image setCenter:currentPos]; _isAnimating = NO; }
- Берём presentation layer, именно там крутится анимация и содержится актуальная информация о состоянии объекта во время её проигрывания – это особенность работы фреймворка Core Graphics. Если этого не сделать, то картинка будет прыгать туда, откуда начиналась анимация.
- Убираем нашу анимацию.
Добавляем метод делегата анимации:
- (void) animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { if (flag) [self stop]; }
Здесь всё просто: если анимация закончилась сама, мы её останавливаем и делаем необходимые действия. В случае принудительного прерывания, остановим её в другом месте. Вот тут, в обработчике касания:
- (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); }
Тут мы просто соединяем всё написаное ранее и освобождаем созданый путь.
Осталось добавить отладочный метод для отрисовки пути:
- (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]; }
- Убираем предидущий путь с экрана, иначе будет каша
- Создаём специальный объект для отрисовки пути. Его код будет ниже.
Наконец, освобождаем ресурсы:
- (void) viewDidUnload { [_image release]; self.pathView = nil; }
Вот и всё, теперь можно запускать.
Приложение
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 Код проекта
Core Animation Programming Guide — Описание тонкостей работы фреймворка.
CGPathRef reference — А также, функций для работы с этой структурой.