Анімовані Лінії в iOS

Доброго часу доби iOS-розробники і їм співчуваючі! Хочу поділитися з вами однією простою, але в той же час досить симпатичною анімацією для текстових полів та інших в'юх на iOS. Думаю, кожен, хто хоча б мигцем стикався з CALayer і Core Animation взагалі, знає про ці можливості, а от новачкам може бути цікаво і наштовхне на вивчення більш глибоко Core Animation.

Картинка для затравки:


Для тих хто не любить читати, а відчувати в дії — посилання на тестовий проект. Для решти ж — Почнемо!

Для тестів створюємо новий проект Single View Application. Додаємо на основний View Controller нову View.

Заголовок спойлера

Створюємо Referencing Outlet з ім'ям 'panel' в клас ViewController. У viewDidLoad ViewController'а додаємо рядок:

_panel.layer.cornerRadius = 5;

Щоб заокруглити кути прямокутника. Запускаємо — зараз додаток виглядає так:


На цьому з Interface Builder ми закінчили. Починається власне те, заради чого ми тут — анімація!

Невеликий екскурс в Core Animation. Базовий клас відтворення в iOS це CALayer, який надає базові можливості для анімації і рендеринга — як то переміщення, трансформації. Загалом це щось середнє між низькорівневої відображенням через Core Graphics і більш високою у вигляді UIView. У нашому випадку нам цікавий спадкоємець CALayer — CAShapeLayer, в якому додається підтримка CGPath, а також супутні методи для цього, як то заливка і робота з stroke (риса?).

Отже. Створимо категорію, що розширює клас UIView UIView+AnimatedLines. Для початку додамо простий метод додавання анимированой обведення VIew з використання CAShapeLayer.

-(void)animateLinesWithColor:(CGColorRef)lineColor andLineWidth:(CGFloat)lineWidth animationDuration:(CGFloat)duration
{

}

Створимо CAShapeLayer:

CAShapeLayer* animateLayer = [CAShapeLayer layer];
animateLayer.lineCap = kCALineCapRound;// Кінець і початок лінії будуть заокругленими
animateLayer.lineJoin = kCALineJoinBevel;//Перехід між лініями буде заоккругленный
animateLayer.fillColor = [[UIColor clearColor] CGColor];//сам шар прозорий
animateLayer.lineWidth = lineWidth;
animateLayer.strokeEnd = 0.0;

Створимо UIBezierPath, в якому і будемо малювати обведення.

UIBezierPath* path = [UIBezierPath new];
[path setLineWidth:1.0];
[path setLineCapStyle:kCGLineCapRound];
[path setLineJoinStyle:kCGLineJoinRound];

Далі проста геометрія — малюємо лінії вздовж кордону нашої в'юшки (багато коду, безглуздого і нещадного):

CGRect bounds = self.layer.bounds;//Кордону нашої в'ю
CGFloat radius = self.layer.cornerRadius;// визначаємо є у в'юшки округлені краї
CGPoint zeroPoint = bounds.origin; //Початкова точка

BOOL isRounded = radius>0;

if(isRounded)
{
zeroPoint.x = bounds.origin.x+radius; //Є округлені краю - починаємо не з самого кута, а з місця, де закінчується округлений кут.
}

[path moveToPoint:zeroPoint];//Пересуваємо курсор в початкову позицію
//Далі проходимся по всіх 4 сторін. Починаємо зверху
CGPoint nextPoint = CGPointMake(bounds.size.width, 0);
if(isRounded)
{
nextPoint.x-=radius;
}
[path addLineToPoint:nextPoint];
if(isRounded)
{
[path addArcWithCenter:CGPointMake(nextPoint.x, nextPoint.y+radius) radius:radius startAngle:-M_PI_2 endAngle:0 clockwise:YES];//Якщо є скруглення - малюємо дугу.
}
//Права межа
nextPoint = CGPointMake(bounds.size.width, bounds.size.height);
if(isRounded)
{
nextPoint.y-=radius;
}
[path addLineToPoint:nextPoint];
if (isRounded)
{
[path addArcWithCenter:CGPointMake(nextPoint.x-radius, nextPoint.y) radius:radius startAngle:0 endAngle:M_PI_2 clockwise:YES];
}
//Нижня межа
nextPoint = CGPointMake(0, bounds.size.height);
if(isRounded)
{
nextPoint.x +=radius;
}
[path addLineToPoint:nextPoint];
if (isRounded)
{
[path addArcWithCenter:CGPointMake(nextPoint.x, nextPoint.y-radius) radius:radius startAngle:M_PI_2 endAngle:M_PI clockwise:YES];
}
//Ліва межа
nextPoint = CGPointMake(0, 0);
if(isRounded)
{
nextPoint.y +=radius;
}
[path addLineToPoint:nextPoint];
if (isRounded)
{
[path addArcWithCenter:CGPointMake(nextPoint.x+radius, nextPoint.y) radius:radius startAngle:M_PI endAngle:-M_PI_2 clockwise:YES];
}

Малювання ліній ми закінчили. Додаємо Path в CAShapeLayer:


animateLayer.path = path.CGPath;
animateLayer.strokeColor = lineColor;

А сам шар на нашу в'ю:

[self.layer addSublayer:animateLayer];

Зараз ми можемо вже подивитися статичний результат наших зусиль, для цього в ViewController додаємо:


_panel.layer.cornerRadius = 5;
[_panel animateLinesWithColor:[UIColor redColor].CGColor andLineWidth:2 animationDuration:5];

І можемо запускати:


Ну, насправді так собі, скажете ви? І будете праві, адже такого ж результату можна домогтися, просто зробивши layer.borderWidth=2.

Тут потрібно невеликий відступ.

Коли ви малюєте в Path (UIPath, CGPath) відрізки, кола та інші примітиви — всі вони мають початок і кінець. StrokeEnd у CAShapeLayer означає до якого місця варто малювати цю лінію.

StrokeStart ж в свою чергу вказує з якого місця потрібно починати малювати лінію. Значення повинні лежати в межах 0.0 — 1.0

Наприклад:

Отже, що можна зробити з цією інформацією? Все що нам потрібно — додати кілька рядків коду. В місці де ми створюємо CAShapeLayer додамо ще один рядок:


animateLayer.strokeEnd = 0.0;

Далі після додавання шару створюємо анімацію для проперті strokeEnd:


CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
pathAnimation.duration = duration;
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0 f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0 f];
pathAnimation.autoreverses = NO;
[animateLayer addAnimation:pathAnimation forKey:@"strokeEndAnimation"];

animateLayer.strokeEnd = 1.0;

(Як працюють CABasicAnimation ви можете почитати на офіційному сайті епл

3. Запускаємо!


Як бачите лінія красиво огинає наш UIView. Тепер давайте зробимо щоб було як на КДПВ.

Яка інформація потрібна нам для цього: точка, від якої лінія буде починати свій рух до кордонів. Так як потрібно щоб лінія виляла — ми зробимо це за допомогою розбиття ділянки від стартової точки до точки початку периметра ділянки на яких отклоним лінію з допомогою:

[path addCurveToPoint:controlPoint1:controlPoint2:];

Зробимо, щоб можна було кілька разів запускати анімацію.

Додамо новий клас який буде містити контрольні точки для кривих Безьє:

@interface LinesCurvePoints : NSObject
@property(nonatomic,assign)CGPoint controlPoint1;
@property(nonatomic,assign)CGPoint controlPoint2;
+(instancetype)curvePoints:(CGPoint)point1 point2:(CGPoint)point2;
@end
@implementation LinesCurvePoints

+(instancetype)curvePoints:(CGPoint)point1 point2:(CGPoint)point2
{
LinesCurvePoints* point = [LinesCurvePoints new];
point.controlPoint1 = point1;
point.controlPoint1 = point2;
return point;
}

@end

Додамо нові поля метод:

-(void)animateLinesWithColor:(CGColorRef)lineColor andLineWidth:(CGFloat)lineWidth startPoint:(CGPoint)startFromPoint rollToStroke:(CGFloat)rollToStroke curveControlPoints:(NSArray<LinesCurvePoints*>*)curvePoints animationDuration:(CGFloat)duration

В методі, після визначення zeroPoint додаємо наступний код:


[path moveToPoint:startFromPoint];
long c = curvePoints.count;
for (long i =1; i < =c; i++)
{
float nX = startFromPoint.x + (zeroPoint.x - startFromPoint.x)/©*i;
float nY = startFromPoint.y +(zeroPoint.y - startFromPoint.y)/©*i;


LinesCurvePoints* point = curvePoints[i-1];


[path addCurveToPoint:CGPointMake(nX, nY) controlPoint1:CGPointMake(nX+point.controlPoint1.x,nY+point.controlPoint1.y) controlPoint2:CGPointMake(nX+ point.controlPoint2.y,nY+ point.controlPoint2.y)];

}

Він Розділить ділянку від стартової точки до початку периметра на рівні ділянки і намалює їх за допомогою кривих, з котрольными точками які ми вказали в curveControlPoints. І друга частина, яку нам потрібно додати — рух strokeStart:


pathAnimation = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
pathAnimation.duration = duration*1.2;

pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0 f];
pathAnimation.toValue = [NSNumber numberWithFloat:rollToStroke];
pathAnimation.autoreverses = NO;

[animateLayer addAnimation:pathAnimation forKey:@"strokeStartAnimation"];
animateLayer.strokeStart = rollToStroke;

Додаємо після анімації strokeEnd. Значення для strokeStart на жаль доведеться підбирати емпірично, мені так і не вдалось вирахувати правильну довжину ділянки, якщо малювати його з кривими Безьє.

Фінальний метод код повинен виглядати так:


-(void)animateLinesWithColor:(CGColorRef)lineColor andLineWidth:(CGFloat)lineWidth startPoint:(CGPoint)startFromPoint rollToStroke:(CGFloat)rollToStroke curveControlPoints:(NSArray<LinesCurvePoints*>*)curvePoints animationDuration:(CGFloat)duration

{

CAShapeLayer* animateLayer = [CAShapeLayer layer];
animateLayer.lineCap = kCALineCapRound;
animateLayer.lineJoin = kCALineJoinBevel;
animateLayer.fillColor = [[UIColor clearColor] CGColor];
animateLayer.lineWidth = lineWidth;
animateLayer.strokeEnd = 0.0;

UIBezierPath* path = [UIBezierPath new];
[path setLineWidth:1.0];
[path setLineCapStyle:kCGLineCapRound];
[path setLineJoinStyle:kCGLineJoinRound];



CGRect bounds = self.layer.bounds;
CGFloat radius = self.layer.cornerRadius;
CGPoint zeroPoint = bounds.origin;




BOOL isRounded = radius>0;

if(isRounded)
{
zeroPoint.x = bounds.origin.x+radius;
}

[path moveToPoint:startFromPoint];

long c = curvePoints.count;
for (long i =1; i < =c; i++)
{
float nX = startFromPoint.x + (zeroPoint.x - startFromPoint.x)/©*i;
float nY = startFromPoint.y +(zeroPoint.y - startFromPoint.y)/©*i;


LinesCurvePoints* point = curvePoints[i-1];


[path addCurveToPoint:CGPointMake(nX, nY) controlPoint1:CGPointMake(nX+point.controlPoint1.x,nY+point.controlPoint1.y) controlPoint2:CGPointMake(nX+ point.controlPoint2.y,nY+ point.controlPoint2.y)];

}

[path moveToPoint:zeroPoint];

CGPoint nextPoint = CGPointMake(bounds.size.width, 0);
if(isRounded)
{
nextPoint.x-=radius;
}
[path addLineToPoint:nextPoint];
if(isRounded)
{
[path addArcWithCenter:CGPointMake(nextPoint.x, nextPoint.y+radius) radius:radius startAngle:-M_PI_2 endAngle:0 clockwise:YES];
}

nextPoint = CGPointMake(bounds.size.width, bounds.size.height);
if(isRounded)
{
nextPoint.y-=radius;
}
[path addLineToPoint:nextPoint];
if (isRounded)
{
[path addArcWithCenter:CGPointMake(nextPoint.x-radius, nextPoint.y) radius:radius startAngle:0 endAngle:M_PI_2 clockwise:YES];
}

nextPoint = CGPointMake(0, bounds.size.height);
if(isRounded)
{
nextPoint.x +=radius;
}
[path addLineToPoint:nextPoint];
if (isRounded)
{
[path addArcWithCenter:CGPointMake(nextPoint.x, nextPoint.y-radius) radius:radius startAngle:M_PI_2 endAngle:M_PI clockwise:YES];
}

nextPoint = CGPointMake(0, 0);
if(isRounded)
{
nextPoint.y +=radius;
}
[path addLineToPoint:nextPoint];
if (isRounded)
{
[path addArcWithCenter:CGPointMake(nextPoint.x+radius, nextPoint.y) radius:radius startAngle:M_PI endAngle:-M_PI_2 clockwise:YES];
}

animateLayer.path = path.CGPath;
animateLayer.strokeColor = lineColor;

[self.layer addSublayer:animateLayer];



CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
pathAnimation.duration = duration;
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0 f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0 f];
pathAnimation.autoreverses = NO;
[animateLayer addAnimation:pathAnimation forKey:@"strokeEndAnimation"];

animateLayer.strokeEnd = 1.0;

pathAnimation = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
pathAnimation.duration = duration*1.2;



pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0 f];
pathAnimation.toValue = [NSNumber numberWithFloat:rollToStroke];
pathAnimation.autoreverses = NO;


[animateLayer addAnimation:pathAnimation forKey:@"strokeStartAnimation"];
animateLayer.strokeStart = rollToStroke;


}

Виклик методу в ViewController:


[_panel animateLinesWithColor:[UIColor redColor].CGColor
andLineWidth:2
startPoint:CGPointMake(100, -200)
rollToStroke:0.25
curveControlPoints:@[
[LinesCurvePoints curvePoints:CGPointMake(-50, -2) point2:CGPointMake(60, 5)],
[LinesCurvePoints curvePoints:CGPointMake(-60, 10) point2:CGPointMake(100, 5)]
] 
animationDuration:2 ];

rollToStroke значення підходить для якщо _panel розміром 240 на 128 пікселів:


Ще один з прикладів використання цієї анімації:


Є багато ігор, заснованих на цій анімації, моя улюблена:



Загалом ось таким нехитрим способом можна зробити досить цікаві анімації в додатку. Буду радий якщо комусь здалося корисним.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.