IOS: circle animation in a wider

Core-Animation handles corners as described in this image: (image from http://btk.tillnagel.com/tutorials/rotation-translation-matrix.html )

EDIT: adding an animated gif to better explain what I need:

enter image description here enter image description here

I need to animate the slice so that it expands, starting from 300: 315 degrees and ending with 300: 060.

To create each slice, I use this function:

extension CGFloat { func toRadians() -> CGFloat { return self * CGFloat(Double.pi) / 180.0 } } func createSlice(angle1:CGFloat, angle2:CGFloat) -> UIBezierPath! { let path: UIBezierPath = UIBezierPath() let width: CGFloat = self.frame.size.width/2 let height: CGFloat = self.frame.size.height/2 let centerToOrigin: CGFloat = sqrt((height)*(height)+(width)*(width)); let ctr: CGPoint = CGPoint(x: width, y: height) path.move(to: ctr) path.addArc( withCenter: ctr, radius: centerToOrigin, startAngle: CGFloat(angle1).toRadians(), endAngle: CGFloat(angle2).toRadians(), clockwise: true ) path.close() return path } 

Now I can create two slices and a sublevel with a smaller one, but I cannot find how to proceed from this point:

 func doStuff() { path1 = self.createSlice(angle1: 300,angle2: 315) path2 = self.createSlice(angle1: 300,angle2: 60) let shapeLayer = CAShapeLayer() shapeLayer.path = path1.cgPath shapeLayer.fillColor = UIColor.cyan.cgColor self.layer.addSublayer(shapeLayer) 

I would really appreciate any help here!

+5
source share
3 answers

Only one color

If you want to animate the angle of a solid colored pie segment, like the one asked in your question, you can do this by animating strokeEnd CAShapeLayer .

The "trick" here is to make a very wide line. More specifically, you can create a path that is only an arc (the dashed line in the animation below), at half the radius, and then gives it the full radius as the width of its line. When you live stroking this line, it looks like an orange segment below:

Animating the stroke end of a shape layer

Depending on your use case, you can:

  • create a path from one corner to another corner and animate the course of the end from 0 to 1
  • create a path for the full circle, set the initial move and the end of the stroke to some part of the circle, and the animated move ends from the initial to the final fraction.

If your picture is only one color, then this will be the smallest solution to your problem.

However, if your drawing is more complex (for example, also stroking a pie segment), then this solution simply will not work, and you will have to do something more complicated.


Custom Drawing / Custom Animations

If your drawing of the pie segment is more complex, you will quickly find that you need to create a subclass of the level with custom animated properties. This is a bit more code, some of which may look somewhat unusual 1 - but not as scary as it might seem.

  • This may be one of those things that is even more convenient to do in Objective-C.

Dynamic properties

First, subclass the level with the properties you need. In an Objective-C expression, these properties must be @dynamic , i.e. Not synthesized. This is not the same as dynamic in Swift. Instead, we should use @NSManaged .

 class PieSegmentLayer : CALayer { @NSManaged var startAngle, endAngle, strokeWidth: CGFloat @NSManaged var fillColor, strokeColor: UIColor? // More to come here ... } 

This allows Core Animation to dynamically process these properties, allowing you to track changes and integrate them into the animation system.

Note A good rule of thumb is that these properties should all be related to the drawing / visual representation of the layer. If this is not the case, then it is likely that they do not belong to the layer. Instead, they can be added to a view, which in turn uses a layer to draw it.

Copy layers

During custom animation, Core Animation will want to create and display different layer configurations for different frames. Unlike most other Apple infrastructures, this happens with the init(layer:) copy constructor. In order for these five properties to be copied, we need to override init(layer:) and copy their values.

In Swift, we must also override simple init() and init?(coder) .

 override init(layer: Any) { super.init(layer: layer) guard let other = layer as? PieSegmentLayer else { return } fillColor = other.fillColor strokeColor = other.strokeColor startAngle = other.startAngle endAngle = other.endAngle strokeWidth = other.strokeWidth } override init() { super.init() } required init?(coder aDecoder: NSCoder) { return nil } 

Reaction to change

Core Animation is largely built for performance. One way to achieve this is to avoid unnecessary work. By default, the layer will not be redrawn when the property changes. But these properties are used for drawing, and we want the layer to be redrawn when any of them changes. To do this, we need to override needsDisplay(forKey:) and return true if the key was one of these properties.

 override class func needsDisplay(forKey key: String) -> Bool { switch key { case #keyPath(startAngle), #keyPath(endAngle), #keyPath(strokeWidth), #keyPath(fillColor), #keyPath(strokeColor): return true default: return super.needsDisplay(forKey: key) } } 

In addition, if we want to use the default implicit animations for these properties, we need to override action(forKey:) to return a partially configured animation object. If we want some properties (for example, angles) to be implicitly animated, we need to return the animation only for these properties. If we donโ€™t need something very ordinary, itโ€™s useful to simply return the base animation with fromValue set to the current view value:

 override func action(forKey key: String) -> CAAction? { switch key { case #keyPath(startAngle), #keyPath(endAngle): let anim = CABasicAnimation(keyPath: key) anim.fromValue = presentation()?.value(forKeyPath: key) return anim default: return super.action(forKey: key) } } 

Drawing

The final piece of custom animation is a custom drawing. This is done by overriding draw(in:) and using the provided context to draw the layer:

 override func draw(in ctx: CGContext) { let center = CGPoint(x: bounds.midX, y: bounds.midY) // subtract half the stroke width to avoid clipping the stroke let radius = min(center.x, center.y) - strokeWidth / 2 // The two angle properties are in degrees but CG wants them in radians. let start = startAngle * .pi / 180 let end = endAngle * .pi / 180 ctx.beginPath() ctx.move(to: center) ctx.addLine(to: CGPoint(x: center.x + radius * cos(start), y: center.y + radius * sin(start))) ctx.addArc(center: center, radius: radius, startAngle: start, endAngle: end, clockwise: start > end) ctx.closePath() // Configure the graphics context if let fillCGColor = fillColor?.cgColor { ctx.setFillColor(fillCGColor) } if let strokeCGColor = strokeColor?.cgColor { ctx.setStrokeColor(strokeCGColor) } ctx.setLineWidth(strokeWidth) ctx.setLineCap(.round) ctx.setLineJoin(.round) // Draw ctx.drawPath(using: .fillStroke) } 

Here I filled and stroked the pie segment, which extends from the center of the layer to the nearest edge. You must replace this with your own drawing.

Custom animation in action

With all this code, we now have a custom level subclass whose properties can be animated both implicitly (simply by changing them) and explicitly (by adding CAAnimation for our key). The results look something like this:

Custom layer subclass animation

Final words

This may not be obvious at the frame rate of these animations, but one strong advantage of using Core Animation (in different ways) in both of these solutions is that it separates the drawing of one state from the time of the animation.

This means that the layer does not and should not know about duration, delays, time curves, etc. All of them can be configured and controlled externally.

+7
source

So finally I found a solution. It took me a while to realize that there is no way to animate the shape of the figure, but we can fool the CA mechanism by creating a filled circle, making a stroke (i.e. the boundary of the arc) extremely wide, so that it fills the whole circle!

 extension CGFloat { func toRadians() -> CGFloat { return self * CGFloat(Double.pi) / 180.0 } } import UIKit class SliceView: UIView { let circleLayer = CAShapeLayer() var fromAngle:CGFloat = 30 var toAngle:CGFloat = 150 var color:UIColor = UIColor.magenta override init(frame: CGRect) { super.init(frame: frame) setup() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } convenience init(frame:CGRect, fromAngle:CGFloat, toAngle:CGFloat, color:UIColor) { self.init(frame:frame) self.fromAngle = fromAngle self.toAngle = toAngle self.color = color } func setup() { circleLayer.strokeColor = color.cgColor circleLayer.fillColor = UIColor.clear.cgColor layer.addSublayer(circleLayer) layer.backgroundColor = UIColor.brown.cgColor } override func layoutSubviews() { super.layoutSubviews() let startAngle:CGFloat = (fromAngle-90).toRadians() let endAngle:CGFloat = (toAngle-90).toRadians() let center = CGPoint(x: bounds.midX, y: bounds.midY) let radius = min(bounds.width, bounds.height) / 4 let path = UIBezierPath(arcCenter: CGPoint(x: 0,y :0), radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) circleLayer.position = center circleLayer.lineWidth = radius*2 circleLayer.path = path.cgPath } public func animate() { let pathAnimation = CABasicAnimation(keyPath: "strokeEnd") pathAnimation.duration = 3.0; pathAnimation.fromValue = 0.0; pathAnimation.toValue = 1.0; circleLayer.add(pathAnimation, forKey: "strokeEndAnimation") } } 

So now we can add it to our view controller and start the animation. In my case, I am connecting it to Objecive-C, but you can easily adapt it to fast.

I just cannot believe that in 2017 it was still impossible to find a turnkey solution for this simple task. It took me days to do this. I really hope this helps others!

This is how I use my class:

 @implementation ViewController { SliceView *sv_; } - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = UIColor.grayColor; CGFloat width = 240.0; CGFloat height = 160.0; CGRect r = CGRectMake( self.view.frame.size.width/2 - width/2, self.view.frame.size.height/2 - height/2, width, height); sv_ = [[SliceView alloc] initWithFrame:r fromAngle:150 toAngle:30 color:[UIColor yellowColor] ]; [self.view addSubview:sv_]; } - (IBAction)pressedGo:(id)sender { [sv_ animate]; } 
+2
source

I am adding a slight improvement for David's class. (David - you can copy in your answer to the quality of the book!)

You can add the following init function:

 convenience init(frame:CGRect, startAngle:CGFloat, endAngle:CGFloat, fillColor:UIColor, strokeColor:UIColor, strokeWidth:CGFloat) { self.init() self.frame = frame self.startAngle = startAngle self.endAngle = endAngle self.fillColor = fillColor self.strokeColor = strokeColor self.strokeWidth = strokeWidth } 

and then call it like this (Objective-C in my case):

 PieSegmentLayer *sliceLayer = [[PieSegmentLayer alloc] initWithFrame:r startAngle:30 endAngle:180 fillColor:[UIColor cyanColor] strokeColor:[UIColor redColor] strokeWidth:4]; [self.view.layer addSublayer:sliceLayer]; 
0
source

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


All Articles