Circle scale animation works a bit weird - swift

I am animating my GMSMarker so that it pulse once in couple seconds.
func addWave()
{
// circleView.layer.cornerRadius = size / 2
//Scale
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = 1
scaleAnimation.toValue = zoom
//Opacity
let alphaAnimation = CABasicAnimation(keyPath: "opacity")
alphaAnimation.toValue = 0.0
//Corner radius
// let cornerRadiusAnimation = CABasicAnimation(keyPath: "cornerRadius")
// cornerRadiusAnimation.fromValue = size / 2
// cornerRadiusAnimation.toValue = (size * zoom)/2
//Animation Group
let animations: [CAAnimation] = [scaleAnimation, alphaAnimation]
let animationGroup = CAAnimationGroup()
animationGroup.duration = duration
animationGroup.animations = animations
animationGroup.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animationGroup.repeatCount = 1
animationGroup.fillMode = kCAFillModeForwards
animationGroup.isRemovedOnCompletion = false
circleView.layer.add(animationGroup, forKey: "group")
}
The result looks like this:
And if I uncomment Corner radius section it looks like this:
So I need an advice.

Based on my observations, I think it must be a issue of wrong path of your CAShapeLayer hence the masking of the circle.
I just wrote a radar animation, I hope it might help you.
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = 0
scaleAnimation.toValue = 1
let alphaAnimation = CABasicAnimation(keyPath: "opacity")
alphaAnimation.fromValue = 1
alphaAnimation.toValue = 0
let animations = CAAnimationGroup()
animations.duration = 0.8
animations.repeatCount = Float.infinity
animations.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animations.animations = [scaleAnimation, alphaAnimation]
circleView.layer.add(animations, forKey: "animations")
What I did was, I used two CAShapeLayer (one being the orange marker and other is the rader layer at the back of marker). The animations were applied on the radar layer.
radarLayer = CAShapeLayer()
radarLayer.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height);
radarLayer.path = UIBezierPath(rect: radarLayer.frame).cgPath
radarLayer.fillColor = UIColor.orange.cgColor
radarLayer.position = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2)
radarLayer.cornerRadius = radarLayer.frame.size.width/2
radarLayer.masksToBounds = true
radarLayer.opacity = 0
self.layer.addSublayer(radarLayer)
circleLayer = CAShapeLayer()
circleLayer.frame = CGRect(x: 0, y: 0, width: 16, height: 16);
circleLayer.path = UIBezierPath(rect: circleLayer.frame).cgPath
circleLayer.fillColor = UIColor.orange.cgColor
circleLayer.position = CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2)
circleLayer.cornerRadius = circleLayer.frame.size.width/2
circleLayer.masksToBounds = true
self.layer.addSublayer(circleLayer)
PS. I'm new to Swift, so enlighten me if I'm wrong somewhere :)
Example : http://i.imgur.com/v2jFWgw.jpg

The following in an excerpt from Google documentation on Markers:
The view behaves as if clipsToBounds is set to YES, regardless of its actual value. You can apply transforms that work outside the bounds, but the object you draw must be within the bounds of the object. All transforms/shifts are monitored and applied. In short: subviews must be contained within the view.
The consequence of that, at least for my case, was exactly the behaviour mentioned in the question: a squared circle animation. The image I wanted to add inside the marker, at the end of the animation, was bigger than the container, so once added to the marker, it was clipped to the bounds of it.
The following is what I did:
public var pulseImageView: UIImageView = {
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
imageView.image = PaletteElements.pulseLocation.value
imageView.contentMode = .center
let pulseAnimation = CABasicAnimation(keyPath: "transform.scale.xy")
pulseAnimation.repeatCount = Float.infinity
pulseAnimation.fromValue = 0
pulseAnimation.toValue = 2.0
pulseAnimation.isRemovedOnCompletion = false
let fadeOutAnimation = CABasicAnimation(keyPath: "opacity")
fadeOutAnimation.duration = 2.5
fadeOutAnimation.fromValue = 1.0
fadeOutAnimation.toValue = 0
fadeOutAnimation.repeatCount = Float.infinity
let animationGroup = CAAnimationGroup()
animationGroup.duration = 2.5
animationGroup.animations = [pulseAnimation, fadeOutAnimation]
animationGroup.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
animationGroup.repeatCount = .greatestFiniteMagnitude
animationGroup.fillMode = CAMediaTimingFillMode.forwards
animationGroup.isRemovedOnCompletion = false
imageView.layer.add(animationGroup, forKey: "pulse")
return imageView
}()
I defined a var inside my helper class so I can retrieve the image whenever I need it. My original image, the pulse image, was 116x116, so I created the imageView as 300x300 with a contentMode = .center, so the small image was in the center and not stretched. I chose 300x300 because my animation scaled up the pulse image until a 2x its initial value (116x2), so I made room for the entire animation to be performed.
Finally I added it to as a Marker to the map:
let pulseMarker = GMSMarker(position: userLocation.coordinate)
pulseMarker.iconView = pulseImageView
pulseMarker.groundAnchor = CGPoint(x: 0.5, y: 0.5)
pulseMarker.map = googleMapView
Hope it can help.

Related

How to achieve this animation with multiple CAShapeLayer

I'm having a lot of trouble exporting a video with animated layers.
The final goal is to export to a video (that's why I need this done with CALayers) a moving CAShapeLayer around the screen and a line following its trace.
First of all, I have an array of CGPoint with all the coordinate points which a CAShapeLayer should use to animate itself. To create the animation I'm using a CAKeyframeAnimation.
let values = [CGPoint(x: 50, y: 100),
CGPoint(x: 100, y: 150)] //Example values
let animation = CAKeyframeAnimation()
animation.keyPath = "position"
animation.values = values
animation.duration = videoLength
With this, I'm able to show the CALayer moving around the screen. It's 1 value per frame of the video, it's a 1:1 value.
The real problem comes with adding a line that shows the path of the CAShapeLayer.
What I think I might do, is to create lines with UIBezierPath, animate them and add each line
lines.forEach { (line) in
let path = UIBezierPath()
let shapeLayer = CAShapeLayer()
for (index, point) in line.points.enumerated() {
if index == 0 {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = line.color.cgColor
shapeLayer.lineWidth = 4
shapeLayer.position = animatedLayer.position
shapeLayer.path = path.cgPath
shapeLayer.opacity = 0
shapeLayers.append(shapeLayer)
}
And to animate the lines...
var timeOffset: Double = 0
let eachLineTime = totalTime / Double(shapeLayers.count)
for shapeLayer in shapeLayers {
CATransaction.begin()
mainLayer.addSublayer(shapeLayer)
let strokeAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeAnimation.fromValue = 0
strokeAnimation.toValue = 1
strokeAnimation.duration = eachLineTime
strokeAnimation.beginTime = CACurrentMediaTime() + timeOffset
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 0
opacityAnimation.toValue = 1
opacityAnimation.duration = eachLineTime
opacityAnimation.beginTime = CACurrentMediaTime() + timeOffset
CATransaction.setCompletionBlock {
shapeLayer.strokeColor = UIColor.orange.cgColor
shapeLayer.opacity = 1
}
shapeLayer.add(strokeAnimation, forKey: nil)
shapeLayer.add(opacityAnimation, forKey: nil)
timeOffset += eachLineTime
CATransaction.commit()
}
I need them to be different lines because I want to change the color of each line when it finishes each animation.
I'm getting the next output in Swift Playgrounds
But I'm not getting anything when I export the video in the app.
How can I simply add a trail of the first CAShapeLayer to see the (square in this case) moving path?
How can I move the CABasicAnimation with the same timing of the CAKeyframeAnimation?

How can I make chained animation using Core Animation in Swift?

I would like to do animation1 first, then animation2. I would also like to repeat them forever.
let animation1 = CAKeyframeAnimation(keyPath: #keyPath(CALayer.position))
animation1.path = circularFirstPath.cgPath
let animation2 = CAKeyframeAnimation(keyPath: #keyPath(CALayer.position))
animation2.path = circularSecondPath.cgPath
circleView3.layer.add(animation1, forKey: nil)
circleView3.layer.add(animation2, forKey: nil)
If you want to lead layer along path:
let segmentDuration: CFTimeInterval = 2.0
let rect = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0)
let path = UIBezierPath(rect: rect)
let posAnimation = CAKeyframeAnimation(keyPath: "position")
posAnimation.duration = segmentDuration
posAnimation.path = path.cgPath
posAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
posAnimation.calculationMode = .paced
posAnimation.repeatCount = .infinity
anyLayer.add(posAnimation, forKey: "position")
If you want to change position try something like this:
let segmentDuration: CFTimeInterval = 2.0
let points = [CGPoint(x: 0, y: 0), CGPoint(x: 400.0, y: 0.0), CGPoint(x: 200.0, y: 200.0)]
let posAnimation = CABasicAnimation(keyPath: "position")
posAnimation.duration = segmentDuration
posAnimation.fromValue = points[0]
posAnimation.toValue = points[1]
let posAnimation1 = CABasicAnimation(keyPath: "position")
posAnimation1.beginTime = posAnimation.duration
posAnimation1.duration = segmentDuration
posAnimation1.fromValue = points[1]
posAnimation1.toValue = points[2]
let posAnimation2 = CABasicAnimation(keyPath: "position")
posAnimation2.beginTime = posAnimation.duration + posAnimation1.duration
posAnimation2.duration = segmentDuration
posAnimation2.fromValue = points[2]
posAnimation2.toValue = points[0]
let positionChain: CAAnimationGroup = CAAnimationGroup()
positionChain.animations = [posAnimation, posAnimation1, posAnimation2]
positionChain.duration = posAnimation.duration + posAnimation1.duration + posAnimation2.duration
positionChain.fillMode = CAMediaTimingFillMode.forwards
positionChain.repeatCount = .infinity
anySubLayer.add(positionChain, forKey: "positionChain")
If you want to change shape of main layer just create CAShapeLayer with shape what you need, mask your layer and start animation:
let segmentDuration: CFTimeInterval = 2.0
let rect = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0)
let shapeLayer = CAShapeLayer()
shapeLayer.path = UIBezierPath(rect: rect).cgPath
shapeLayer.frame = CGRect(x: 0, y: 0, width: 10, height: 10)
layer.mask = shapeLayer
let paths = [UIBezierPath(rect: rect), UIBezierPath(roundedRect: rect, cornerRadius: 25), UIBezierPath(ovalIn: rect)]
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.duration = segmentDuration
pathAnimation.fromValue = paths[0].cgPath
pathAnimation.toValue = paths[1].cgPath
let pathAnimation1 = CABasicAnimation(keyPath: "path")
pathAnimation1.beginTime = pathAnimation.duration
pathAnimation1.duration = segmentDuration
pathAnimation1.fromValue = paths[1].cgPath
pathAnimation1.toValue = paths[2].cgPath
let pathAnimation2 = CABasicAnimation(keyPath: "path")
pathAnimation2.beginTime = pathAnimation.duration + pathAnimation1.duration
pathAnimation2.duration = segmentDuration
pathAnimation2.fromValue = paths[2].cgPath
pathAnimation2.toValue = paths[0].cgPath
let pathChain: CAAnimationGroup = CAAnimationGroup()
pathChain.animations = [pathAnimation, pathAnimation1, pathAnimation2]
pathChain.duration = pathAnimation.duration + pathAnimation1.duration + pathAnimation2.duration
pathChain.fillMode = CAMediaTimingFillMode.forwards
pathChain.repeatCount = .infinity
shapeLayer.add(pathChain, forKey: "positionChain")

Swift - how to animatedly draw line NOT rect? Can only draw rect?

I have looked around about this before but have yet to find an answer. I've been at this for a while and need to know how to animate a LINE as opposed to a rectangle.
From what I can see the animation is very different going from a stroke to a box. Just need some pointers here-
I have a border that is drawn on view load (not with an animation, it just draws it) here:
override public func drawRect(rect: CGRect){
super.drawRect(rect)
let borderColor = self.hasError! ? kDefaultActiveColor : kDefaultErrorColor
let textRect = self.textRectForBounds(rect)
let context = UIGraphicsGetCurrentContext()
let borderlines : [CGPoint] = [CGPointMake(0, CGRectGetHeight(textRect) - 1),
CGPointMake(CGRectGetWidth(textRect), CGRectGetHeight(textRect) - 1)]
if self.enabled {
CGContextBeginPath(context);
CGContextAddLines(context, borderlines, 2);
CGContextSetLineWidth(context, 1.3);
CGContextSetStrokeColorWithColor(context, borderColor.CGColor);
CGContextStrokePath(context);
I got this from another answer and am trying to take it apart. I need the line to animate/grow out from its centre when the view loads, as in grow to the same width and size as it is drawn here but animated.
I do not know how to accomplish this, however another answer achieved the effect I need by setting the border to a size of zero here:
self.activeBorder = UIView(frame: CGRectZero)
self.activeBorder.backgroundColor = kDefaultActiveColor
//self.activeBorder.backgroundColor = kDefaultActiveBorderColor
self.activeBorder.layer.opacity = 0
self.addSubview(self.activeBorder)
and animating using:
self.borderlines.transform = CATransform3DMakeScale(CGFloat(0.01), CGFloat(1.0), 1)
self.activeBorder.layer.opacity = 1
CATransaction.begin()
self.activeBorder.layer.transform = CATransform3DMakeScale(CGFloat(0.03), CGFloat(1.0), 1)
let anim2 = CABasicAnimation(keyPath: "transform")
let fromTransform = CATransform3DMakeScale(CGFloat(0.01), CGFloat(1.0), 1)
let toTransform = CATransform3DMakeScale(CGFloat(1.0), CGFloat(1.0), 1)
anim2.fromValue = NSValue(CATransform3D: fromTransform)
anim2.toValue = NSValue(CATransform3D: toTransform)
anim2.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
anim2.fillMode = kCAFillModeForwards
anim2.removedOnCompletion = false
self.activeBorder.layer.addAnimation(anim2, forKey: "_activeBorder")
CATransaction.commit()
This seems to be using entirely different code than my original drawRect and I don't know how to combine the two. Where am I going wrong here? How can I draw out from the centre my first border as I do the second, active border?
Use CAShapeLater, UIBezierPath and CABasicAnimation to animatedly draw line.
Here is the sample code:
class SampleView: UIView {
override public func drawRect(rect: CGRect) {
let leftPath = UIBezierPath()
leftPath.moveToPoint(CGPointMake(CGRectGetMidX(frame), CGRectGetHeight(frame)))
leftPath.addLineToPoint(CGPointMake(0, CGRectGetHeight(frame)))
let rightPath = UIBezierPath()
rightPath.moveToPoint(CGPointMake(CGRectGetMidX(frame), CGRectGetHeight(frame)))
rightPath.addLineToPoint(CGPointMake(CGRectGetWidth(frame), CGRectGetHeight(frame)))
let leftShapeLayer = CAShapeLayer()
leftShapeLayer.path = leftPath.CGPath
leftShapeLayer.strokeColor = UIColor.redColor().CGColor;
leftShapeLayer.lineWidth = 1.3
leftShapeLayer.strokeEnd = 0
layer.addSublayer(leftShapeLayer)
let rightShpaeLayer = CAShapeLayer()
rightShpaeLayer.path = rightPath.CGPath
rightShpaeLayer.strokeColor = UIColor.redColor().CGColor;
rightShpaeLayer.lineWidth = 1.3
rightShpaeLayer.strokeEnd = 0
layer.addSublayer(rightShpaeLayer)
let drawLineAnimation = CABasicAnimation(keyPath: "strokeEnd")
drawLineAnimation.toValue = NSNumber(float: 1)
drawLineAnimation.duration = 1
drawLineAnimation.fillMode = kCAFillModeForwards
drawLineAnimation.removedOnCompletion = false
leftShapeLayer.addAnimation(drawLineAnimation, forKey: nil)
rightShpaeLayer.addAnimation(drawLineAnimation, forKey: nil)
}
}
The code in viewDidLoad of ViewController
// ...
let sampleView = SampleView()
sampleView.frame = CGRectMake(10, 60, 240, 50)
sampleView.backgroundColor = UIColor.lightGrayColor()
view.addSubview(sampleView)
Here is the capture:
Note: If you run the code in iOS9 Simulator (I have not tested the earlier version), the animation may get blocked. Choose to run it in a real device or in iOS10 Simulator.

CAShapelayer Duplicates

I'm trying to add a CAShapelayer once every 20ms to given x and y coordinates. I would like the shape to fade away over a second (like a tracer). The function I have created works, the shape is created in the correct location and fades away. But I am getting extra shapes left behind cluttering up the screen.
func shadowBall (x: CGFloat, y: CGFloat){
let xpos : CGFloat = ((self.frame.width/2) + x)
let ypos : CGFloat = ((self.frame.height/2) + y)
let shadowBall = CAShapeLayer()
let shadowBalllRadius :CGFloat = 4
let shadowBallPath : UIBezierPath = UIBezierPath(ovalInRect: CGRect(x: xpos, y: ypos, width: CGFloat(shadowBalllRadius*2), height: CGFloat(shadowBalllRadius*2)))
shadowBall.path = shadowBallPath.CGPath
shadowBall.fillColor = UIColor.clearColor().CGColor
shadowBall.strokeColor = UIColor.whiteColor().CGColor
shadowBall.lineWidth = 0.5
let animation: CABasicAnimation = CABasicAnimation(keyPath: "strokeColor")
animation.fromValue = UIColor.whiteColor().CGColor
animation.toValue = UIColor.clearColor().CGColor
animation.duration = 1.0;
animation.repeatCount = 0;
animation.removedOnCompletion = true
animation.additive = false
self.layer.addSublayer(shadowBall)
shadowBall.addAnimation(animation, forKey: "strokeColor")
}
The problem is that when the animation finishes, it restores the strokeColor to the original color. You should really set the strokeColor of the original shape layer to be clearColor(), that way, when you finish animating from whiteColor() to clearColor(), it will remain at clearColor().
You can also set the layer's fillMode to kCAFillModeForwards and set removedOnCompletion to false and that will have the layer preserve its "end of animation" state. But I personally would just set the strokeColor as outlined above, as using removedOnCompletion of true interferes with animationDidStop (see below).
Also, I might suggest that you also remove the layer once it's done with the animation so it doesn't continue to consume memory although it's no longer visible.
func shadowBall (x: CGFloat, y: CGFloat) {
let xpos: CGFloat = ((self.frame.width/2) + x)
let ypos: CGFloat = ((self.frame.height/2) + y)
let shadowBall = CAShapeLayer()
let shadowBalllRadius: CGFloat = 4
let shadowBallPath = UIBezierPath(ovalInRect: CGRect(x: xpos, y: ypos, width: CGFloat(shadowBalllRadius*2), height: CGFloat(shadowBalllRadius*2)))
shadowBall.path = shadowBallPath.CGPath
shadowBall.fillColor = UIColor.clearColor().CGColor
shadowBall.strokeColor = UIColor.clearColor().CGColor
shadowBall.lineWidth = 0.1
let animation = CABasicAnimation(keyPath: "strokeColor")
animation.fromValue = UIColor.whiteColor().CGColor
animation.toValue = UIColor.clearColor().CGColor
animation.duration = 1.0
animation.repeatCount = 0
animation.removedOnCompletion = true
animation.additive = false
animation.delegate = self
animation.setValue(shadowBall, forKey: "animationLayer")
self.layer.addSublayer(shadowBall)
shadowBall.addAnimation(animation, forKey: "strokeColor")
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if let layer = anim.valueForKey("animationLayer") as? CALayer {
layer.removeFromSuperlayer()
}
}
See How to remove a CALayer-object from animationDidStop?

CAShapeLayer - Circle within UIVIew

I am struggling to draw a circle within a UIView added onto my main view, using Swift 2.0 in Xcode 7.
The UIView has been created in storyboard and declared as an IBOutlet in code:
#IBOutlet var circleView: UIView!
I have used code from a tutorial online to draw a circle in time to a countdown timer:
func createCircle(){
let bounds = CGRect(x: 0, y: 0, width: circleView.frame.width / 2, height: circleView.frame.width / 2)
// Create CAShapeLayer
let rectShape = CAShapeLayer()
rectShape.bounds = bounds
rectShape.position = circleView.center
rectShape.cornerRadius = bounds.width / 2
view.layer.addSublayer(rectShape)
rectShape.path = UIBezierPath(ovalInRect: rectShape.bounds).CGPath
rectShape.lineWidth = (bounds.width) / 10 // chnage this to change line width
rectShape.strokeColor = UIColor.whiteColor().CGColor
rectShape.fillColor = UIColor.clearColor().CGColor
rectShape.strokeStart = 0
rectShape.strokeEnd = 0
let start = CABasicAnimation(keyPath: "strokeStart")
start.toValue = 0
let end = CABasicAnimation(keyPath: "strokeEnd")
end.toValue = 1
let group = CAAnimationGroup()
group.animations = [start, end]
group.duration = timerDuration
group.autoreverses = false // use true to bounce it back
group.repeatCount = 1 // can use the keyword HUGE to repeat forver
rectShape.addAnimation(group, forKey: nil)
}
I have been playing around with the position, but it never seems to center the circle in the center of circleView.
Here is code I use to make a user's profile image round:
imageView.layer.cornerRadius = imageView.frame.size.width / 2
imageView.clipsToBounds = true
In your case, you would use circleView instead of imageView.