Пожалуй, большинство iOs разработчиков знают, что для реализации различных визуальных эффектов, обычно, достаточно нескольких строчек кода. Фреймворк UIKit, отвечающий за стандартный интерфейс, имеет встроенные средства, позволяющие делать довольно изощрённые виды анимации — от перемещения по прямой, до эффекта переворачивания страницы. Однако, для перемещения наследников UIView по более сложной траектории, приходится спускаться ниже и переходить на уровень фреймворка Core Graphics. При этом, количество примеров в сети снижается и бывает сложно найти необходимое. А если и находится, то качество реализации, зачастую, оставляет желать лучшего. С такой ситуацией я и столкнулся, когда возникла необходимость сделать анимацию интерактивной книги для детей.
Механизм анимации
Для реализации движения по произвольной траектории используется следующий подход:
- строится путь, состоящий из фигур (прямые, кривые, окружности и прочее). Для этого используется структура 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;
}
Вот и всё, теперь можно запускать.
Приложение
#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
#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 — А также, функций для работы с этой структурой.
Автор: Kirpa