- PVSM.RU - https://www.pvsm.ru -
Пожалуй, большинство iOs разработчиков знают, что для реализации различных визуальных эффектов, обычно, достаточно нескольких строчек кода. Фреймворк UIKit, отвечающий за стандартный интерфейс, имеет встроенные средства, позволяющие делать довольно изощрённые виды анимации — от перемещения по прямой, до эффекта переворачивания страницы. Однако, для перемещения наследников UIView по более сложной траектории, приходится спускаться ниже и переходить на уровень фреймворка Core Graphics. При этом, количество примеров в сети снижается и бывает сложно найти необходимое. А если и находится, то качество реализации, зачастую, оставляет желать лучшего. С такой ситуацией я и столкнулся, когда возникла необходимость сделать анимацию интерактивной книги для детей.
Для реализации движения по произвольной траектории используется следующий подход:
Пути бывают двух типов: статичный 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 может быть не очевидно и для лучшего понимания посмотрим на приведённую ниже картинку:
А – центр воображаемой окружности, по которой будет пролегать наша дуга. Координаты задают параметры 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 блока:
Перейдём к самой анимации:
- (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;
}
Добавляем метод делегата анимации:
- (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 [1] Код проекта
Core Animation Programming Guide [2] — Описание тонкостей работы фреймворка.
CGPathRef reference [3] — А также, функций для работы с этой структурой.
Автор: Kirpa
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka-pod-ios/28753
Ссылки в тексте:
[1] GitHub: https://github.com/kirpa/Circle-Movement-Demo
[2] Core Animation Programming Guide: https://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/CoreAnimation_guide/Introduction/Introduction.html
[3] CGPathRef reference: https://developer.apple.com/library/ios/#documentation/graphicsimaging/Reference/CGPath/Reference/reference.html
[4] Источник: http://habrahabr.ru/post/171735/
Нажмите здесь для печати.