I wanted to animate an image object by moving it along a particular curve. It is not a general or random curve but rather a curve which follows a particular path on screen.
Currently, Im manually specifying the list of x and y co-ordinates of the path along which i want the image object to move by setting its frame each time. This is a laborious process in the sense that im setting the specific x and y coordinates of the path and moving the image along it. Is there a more efficient way to do this?
Is there a way that i can specify,say, just about 15 - 20 points and have a curve traced along those to move the object? Any other way to acheive this? Any help would be much appreciated. Thanks.
You could use a combination of UIBezierPath and CAKeyFrameAnimation.
I found a very useful blog post dealing with this subject.
http://oleb.net/blog/2010/12/animating-drawing-of-cgpath-with-cashapelayer/
Here's a simplified version of what I used (it just animates the drawing of a square):
UIBezierPath *customPath = [UIBezierPath bezierPath];
[customPath moveToPoint:CGPointMake(100,100)];
[customPath addLineToPoint:CGPointMake(200,100)];
[customPath addLineToPoint:CGPointMake(200,200)];
[customPath addLineToPoint:CGPointMake(100,200)];
[customPath addLineToPoint:CGPointMake(100,100)];
UIImage *movingImage = [UIImage imageNamed:#"foo.png"];
CALayer *movingLayer = [CALayer layer];
movingLayer.contents = (id)movingImage.CGImage;
movingLayer.anchorPoint = CGPointZero;
movingLayer.frame = CGRectMake(0.0f, 0.0f, movingImage.size.width, movingImage.size.height);
[self.view.layer addSublayer:movingLayer];
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.duration = 4.0f;
pathAnimation.path = customPath.CGPath;
pathAnimation.calculationMode = kCAAnimationLinear;
[movingLayer addAnimation:pathAnimation forKey:#"movingAnimation"];
In Swift-3 version of #Jilouc :-
override func viewDidLoad() {
super.viewDidLoad()
addAdditiveAnimation()
initiateAnimation()
}
//curve which follows a particular path
func pathToTrace() -> UIBezierPath {
let path = UIBezierPath(ovalIn: CGRect(x: 120 , y: 120, width: 100, height: 100))
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1.0
self.view.layer.addSublayer(shapeLayer)
return path
}
func addAdditiveAnimation() {
let movement = CAKeyframeAnimation(keyPath: "position")
movement.path = pathToTrace().cgPath
movement.duration = 5
movement.repeatCount = HUGE
movement.calculationMode = kCAAnimationPaced
movement.timingFunctions = [CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)]
self.movement = movement
}
func createLayer() -> CALayer {
let layer = CALayer()
let image = UIImage(named: "launch.png")
layer.frame = CGRect(x: 0 , y: 0, width: (image?.size.width),height: (image?.size.height))
layer.position = CGPoint(x: 5, y: 5)
layer.contents = image?.cgImage
layer.anchorPoint = .zero
layer.backgroundColor = UIColor.red.cgColor
//layer.cornerRadius = 5
self.view.layer.addSublayer(layer)
return layer
}
func initiateAnimation() {
let layer = createLayer()
layer.add(self.movement, forKey: "Object Movement")
}
Github Demo
Related
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?
CAShapeLayer disappears while moving to another monitor, but not always
Tried searching for code examples
override func draw(_ dirtyRect: NSRect)
{
//super.draw(dirtyRect)
image?.lockFocus()
print("SelectionRect::draw")
if (shapeLayerIsVisible) {
auxLayer.removeFromSuperlayer()
shapeLayer.removeFromSuperlayer()
}
//let origin = frame.origin
auxLayer = CAShapeLayer()
auxLayer.strokeColor = NSColor.white.cgColor
auxLayer.fillColor = nil
auxLayer.lineWidth = 1.0
//rectSize = CGSize(width: x2 - x1, height: y2 - y1)
let selectionRect = frame //CGRect(x: origin.x, y: origin.y, width: frame.size.width, height: frame.size.height)
let auxPath = CGMutablePath()
//print("x1: \(x1), y1: \(y1), x2: \(x2), y2: \(y2), width: \(rectSize.width), height: \(rectSize.height)")
auxPath.addRect(selectionRect)
auxLayer.path = auxPath
layer?.addSublayer(auxLayer)
shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = NSColor.black.cgColor
shapeLayer.fillColor = nil
shapeLayer.lineWidth = 1.0
shapeLayer.lineDashPattern = [5, 5]
let path = CGMutablePath()
path.addRect(selectionRect)
shapeLayer.path = path
let lineDashAnimation = CABasicAnimation(keyPath: "lineDashPhase")
lineDashAnimation.fromValue = 0
lineDashAnimation.toValue = shapeLayer.lineDashPattern?.reduce(0) { $0 + $1.intValue }
lineDashAnimation.duration = 1
lineDashAnimation.repeatCount = Float.greatestFiniteMagnitude
shapeLayer.add(lineDashAnimation, forKey: nil)
shapeLayerIsVisible = true
layer?.addSublayer(shapeLayer)
I've got an 5K iMac with two external 4K monitors (all three scaled to look like 2560x1440 if that matters). With the given code, the NSView is initialized with x: 0, y:0, width: 100, height: 100. All works well, I can drag the Windows around on all three monitors. When I moved the Layer with the mouse to a different position and than drag the window to a different monitor the Shape disappears.
Edit: I fixed it by calling draw(9 at the end of each mouseDragged()-Event and chaninging selectionRect from frame to bounds. It works, but is that a clean solution?
Don't call draw() yourself, per Apple's documentation:
You should never call draw() directly yourself. To invalidate part of your view, and thus cause that portion to be redrawn, call the setNeedsDisplay() or setNeedsDisplay(_:) method instead.
I'm creating an IOS application. I have 5 buttons in star shapes created with UIBezierPath. And I want fill them when I tapped specific star. For ex. if I tapped 3-rd star then gradient will fill 3 first stars etc.
I have already created view with 5 stars. In the draw view method I have added code for animating gradient and it works fine only when view is launched.
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
UIColor.black.setStroke()
path.lineWidth = 2
let numberOfPoints = 5
var angle: CGFloat = CGFloat.pi / 2
let angleOfIncrement = CGFloat.pi*2/CGFloat(numberOfPoints)
let radius: CGFloat = rect.origin.x + rect.width/2
let offset = CGPoint(x: rect.origin.x + rect.width/2, y: rect.origin.y + rect.height/2)
let midleRadius = radius*0.45
var firstStar = true
for _ in 1...numberOfPoints {
let firstPoint = getPointLocation(angle: angle, radius: midleRadius, offset: offset)
let midlePoint = getPointLocation(angle: angle+angleOfIncrement/2, radius: radius, offset: offset)
let secondPoint = getPointLocation(angle: angle+angleOfIncrement, radius: midleRadius, offset: offset)
if firstStar {
firstStar = false
path.move(to: firstPoint)
path.addLine(to: midlePoint)
path.addLine(to: secondPoint)
}else {
path.addLine(to: firstPoint)
path.addLine(to: midlePoint)
path.addLine(to: secondPoint)
}
angle += angleOfIncrement
}
path.stroke()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
let gradient = CAGradientLayer(layer: layer)
gradient.colors = [UIColor.black.cgColor, UIColor.white.cgColor]
gradient.locations = [1]
gradient.startPoint = CGPoint(x: 0, y: 1)
gradient.endPoint = CGPoint(x: 1, y: 1)
gradient.frame = path.bounds
self.layer.addSublayer(gradient)
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [0, 0]
animation.toValue = [0, 1.0]
animation.duration = 3
gradient.add(animation, forKey: nil)
self.layer.mask = shapeLayer
}
I expect that when I tapped 3-rd star then gradient will fill first 3 stars.
You have to separate your drawing code from your animation code, and (btw.) you should also (re-)move the layer creation out of draw().
For the separation move
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [0, 0]
animation.toValue = [0, 1.0]
animation.duration = 3
gradient.add(animation, forKey: nil)
into a separate method and call it when needed.
You must also remove the layer creation from draw()because this method is always called if the view needs to be (re-)drawn. This produce a serious memory leak, because for instance every rotation of your iPhone will create a new gradient layer.
You have another issue in your code. Your gradient layer lies over the layer of the view. That cannot work. You may make the gradient layer to the view's layer, and work with mask, to cut the stars out of the gradient.
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.
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.