Анимированные Линии в iOS

в 9:19, , рубрики: core animation, iOS, UI, разработка под iOS

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

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

Анимированные Линии в iOS - 1

Для тех кто не любит читать, а испытывать в действии — ссылка на тестовый проект. Для всех остальных же — Начнем!

Для тестов создаем новый проект Single View Application. Добавляем на основной View Controller новую View.

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

Анимированные Линии в iOS - 2

Создаем Referencing Outlet с именем 'panel' в класс ViewController. В viewDidLoad ViewController'а добавляем строчку:

_panel.layer.cornerRadius = 5;

Чтобы скруглить углы у прямоугольника. Запускаем — сейчас приложение выглядит так:

Анимированные Линии в iOS - 3

На этом с 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];

И можем запускать:

Анимированные Линии в iOS - 4

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

Тут нужно небольшое отступление.

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

StrokeStart же в свою очередь указывает с какого места нужно начинать рисовать линию. Значение должны лежать в пределах 0.0 — 1.0

Например:

Анимированные Линии в iOS - 5

Итак, что можно сделать с этой информацией? Все что нам нужно — добавить несколько строк кода. В месте где мы создаем CAShapeLayer добавим еще одну строчку:


animateLayer.strokeEnd   = 0.0;

Далее после добавления слоя создаем анимацию для проперти strokeEnd:


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

(Как работают CABasicAnimation вы можете почитать на официальном сайте эпл)

3. Запускаем!

Анимированные Линии в iOS - 6

Как видите линия красиво огибает наш 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)/(c)*i;
		float nY = startFromPoint.y +(zeroPoint.y - startFromPoint.y)/(c)*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.0f];
	pathAnimation.toValue = [NSNumber numberWithFloat:rollToStroke];
	pathAnimation.autoreverses = NO;

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

Добавляем после анимации strokeEnd. Значение для strokeStart к сожалению придется подбирать эмпирически, мне так и не получилось высчитать правильную длинну участка если рисовать его c кривыми Безье.

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


-(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)/(c)*i;
		float nY = startFromPoint.y +(zeroPoint.y - startFromPoint.y)/(c)*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.0f];
	pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
	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.0f];
	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 пикселей:

Анимированные Линии в iOS - 7

Еще один из примеров использования этой анимации:

Анимированные Линии в iOS - 8

Есть много игр основанных на этой анимации, моя любимая:

В общем вот таким нехитрым способом можно сделать довольно интересные анимации в приложении. Буду рад если кому-то показалось полезным.

Автор: DimasSup

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js