⬆️ ⬇️

Animated Lines in iOS

Good day, iOS-developers and their sympathizers! I want to share with you one simple, but at the same time quite nice animation for text fields and other views on iOS. I think everyone who has even had a glimpse of CALayer and Core Animation in general knows about these possibilities, but for beginners it can be interesting and push Corex to explore more deeply.



The picture for the seed:





For those who do not like to read, and experience in action - a link to a test project . For everyone else - Let's start!



For tests we create a new project Single View Application. Add a new View to the main View Controller.

')

Spoiler header




Create a Referencing Outlet with the name 'panel' in the ViewController class. In viewDidLoad ViewController, we add the line:



_panel.layer.cornerRadius = 5; 


To round the corners of the rectangle. We start - now the application looks like this:





We are done with Interface Builder. It actually begins for the sake of what we are here - animation!



A little excursion into Core Animation. The base rendering class in iOS is CALayer, which provides the basic features for animation and rendering - such as moving, transforming. In general, this is somewhere between low-level rendering via Core Graphics and higher in the form of a UIView. In our case, we are interested in the successor of CALayer - CAShapeLayer, which adds support for CGPath, as well as related methods for this, such as pouring and working with stroke (what the hell?).



So. Create a category that extends the UIView class - UIView + AnimatedLines . To begin with, let's add a simple method of adding an animated stroke for VIew using CAShapeLayer.



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


Create a CAShapeLayer:



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


Create a UIBezierPath in which we will draw a stroke.



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


Next, simple geometry - draw lines along the border of our view (a lot of code, meaningless and merciless):



 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]; } 


Line drawing we finished. Add Path to CAShapeLayer:



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


And the layer itself on our twist:



 [self.layer addSublayer:animateLayer]; 


Now we can already see the static result of our work, for this we add to the ViewController:



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


And we can run:





Well, really so-so, you say? And you will be right, because the same result can be achieved simply by making layer.borderWidth = 2.



Here you need a small digression.



When you draw in Path (UIPath, CGPath) segments, circles and other primitives - they all have a beginning and an end. StrokeEnd at CAShapeLayer means to what place it is worth drawing this line.



StrokeStart, in turn, indicates where to start drawing a line. The value should lie in the range 0.0 - 1.0



For example:





So what can you do with this information? All we need to do is add a few lines of code. In the place where we create CAShapeLayer we will add one more line:



 animateLayer.strokeEnd = 0.0; 


Then, after adding the layer, create an animation for the 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; 


(How do CABasicAnimation work you can read on the official website of the epl )



3. Run!





As you can see the line beautifully rounds our UIView. Now let's make it like on the KDPV.



What information we need for this: the point from which the line will begin its movement to the borders. Since it is necessary to wag the line - we will do this by splitting the section from the starting point to the point of the beginning of the perimeter into sections on which we will deflect the line with:



 [path addCurveToPoint:controlPoint1:controlPoint2:]; 


Let's make it possible to run the animation several times.



Add a new class that will contain control points for Bezier curves:



 @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 


Add new fields to the method:



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


In the method, after determining zeroPoint, add the following code:



 [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)]; } 


It will divide the section from the starting point to the beginning of the perimeter into equal sections and draw them using the curves with the control points that we specified in curveControlPoints. And the second part we need to add is the strokeStart motion:



 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; 


Add after the strokeEnd animation. Unfortunately, the value for strokeStart will have to be selected empirically, I never managed to calculate the correct length of the section if I draw it with Bezier curves.



The final method code should look like this:



 -(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; } 


Calling a method in 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 value is suitable for if the _panel is 240 pixels by 128 pixels:





Another example of using this animation:





There are many games based on this animation, my favorite:







In general, in such a simple way you can make quite interesting animations in the application. I would be glad if someone found it useful.

Source: https://habr.com/ru/post/318716/



All Articles