CABasicAnimation - Continue animation from Presentation Layer but not as fromValue - ios

I am working on a looping animation that can be cancelled when desired. When the animation is cancelled another animation is called to animate the Presentation Layer back to the fromValue of the previous animation.
The looping animation:
var toBounds = layer.bounds
toBounds.size.height = 12
let animation = CABasicAnimation(keyPath: "bounds")
animation.fromValue = layer.bounds
animation.toValue = toBounds
animation.repeatCount = HUGE
animation.autoreverses = true
animation.duration = 0.3
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
layer.removeAllAnimations()
layer.add(animation, forKey: "oscillation")
The cancel animation:
if let presentationLayer = layer.presentation() {
let animation = CABasicAnimation(keyPath: "bounds")
animation.fromValue = presentationLayer.bounds
animation.toValue = layer.bounds
animation.duration = 0.3
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
layer.removeAllAnimations()
layer.add(animation, forKey: "decay")
}
This makes the animation look smooth when returning to it's original state. However, when the animation gets restarted while the cancel animation is still busy, the animation has this stuttering effect. What I tried to do is change the fromValue in the looping animation but this means that the animation doesn't return to the desired fromValue. How can I make this restart smooth again? Is there a way to set the animation's current value?

Related

Jumpy scrolling of CollectionView with CABasicAnimation in cell

I have a CollectionView in which the cell contains circular progress indicator view with CABasicAnimation.
func _animateShapeLayer(_ layer: CAShapeLayer, percent: CGFloat) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = percent
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
animation.duration = 1
layer.add(animation, forKey: "animateStrokeEnd")
layer.strokeEnd = percent
}
This is causing scrolling jumpy. I have set should shouldRasterize to true for the cell as well as rasterizationScale set to main scale. Still its of no use. Help me please.

iOS animating object goes back to original position

I am trying to animate the position of a UIView object with CABasicAnimation on a button Tap. The object animated and moves to the 'to' position, but returns back to the original position after the animation ends. I want to retain the position of the view object even after the animation ends. This the code snippet that performs the animation. viewObject is the object which I'm trying to animate.
let animation = CABasicAnimation(keyPath: "position")
animation.timingFunction = CAMediaTimingFunction(controlPoints: 0.86, 0, 0.07, 1.0)
animation.duration = 0.5
animation.fromValue = NSValue(cgPoint: CGPoint(x: viewObject.center.x, y: viewObject.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: viewObject.center.x + 64, y: viewObject.center.y))
viewObject.layer.add(animation, forKey: "position")
add following lines before adding animation
animation.isRemovedOnCompletion = false
animation.fillMode = kCAFillModeForwards
Swift
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
Please add the following code:
Objective-C:
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
Swift 4:
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
I think you need to give frame again on completion. So, this can be a nice approach
UIView.animate(withDuration: 0.7, delay: 1.0, options: .curveLinear, animations: {
let animation = CABasicAnimation(keyPath: "position")
animation.timingFunction = CAMediaTimingFunction(controlPoints: 0.86, 0, 0.07, 1.0)
animation.duration = 0.5
animation.fromValue = NSValue(cgPoint: CGPoint(x: self.viewObject.center.x, y: self.viewObject.center.y))
animation.toValue = NSValue(cgPoint: CGPoint(x: self.viewObject.center.x + 64, y: self.viewObject.center.y))
self.viewObject.layer.add(animation, forKey: "position")
}, completion: { finished in
self.viewObject.frame.origin.x = self.viewObject.frame.origin.x + 64
})
Try this. It will work perfectly
UIView.animateWithDuration(0.7, delay: 1.0, options: .CurveEaseOut, animations:
{
Code
}, completion:
{
finished in
println("Nothing to do!")
})
Just do animation inside a block and when it finishes. hold that position and do not revert back. It should stay at same position
and add this line inside code
cabasicanimation.removedOnCompletion = false;
This line will make it do not go back in the same state
If you want keep your position, you should put your code same my structure
CATransaction.begin()
CATransaction.setDisableActions(true)
CATransaction.setCompletionBlock {
// Input code set new position at here
}
// Intput code animation
CATransaction.commit()

CALayer resizing back to original size after transform animation

I'm trying to implement two consecutive transformation animations. When the first animation ends, the second animation is called through the completion handler. Because this is a transformation animation, my issue is that when the first animation finishes, the layer resizes back to the original size, and then the second animation begins. I'd like for the second animation to begin with the new layer size after the first transformation animation. This post Objective-C - CABasicAnimation applying changes after animation? says I have to resize/transform the layer before beginning the first animation, so that when the first animation ends, the layer is actually the new size. I've tried to do that by changing the bounds or actually applying the transform to the layer but its still not working.
override func viewDidAppear(_ animated: Bool) {
buildBar()
}
func buildBar(){
progressBar1.bounds = CGRect(x: 0, y: 0, width: 20, height: 5)
progressBar1.position = CGPoint(x: 0, y: 600)
progressBar1.backgroundColor = UIColor.white.cgColor
view.layer.addSublayer(progressBar1)
extendBar1()
}
func extendBar1(){
CATransaction.begin()
let transform1 = CATransform3DMakeScale(10, 1, 1)
let anim = CABasicAnimation(keyPath: "transform")
// self.progressBar1.bounds = CGRect(x: 0, y: 0, width: 200, height: 5)
// self.progressBar1.transform = transform1
anim.isRemovedOnCompletion = false
anim.fillMode = kCAFillModeForwards
anim.toValue = NSValue(caTransform3D:transform1)
anim.duration = 5.00
CATransaction.setCompletionBlock {
self.extendBar2()
}
progressBar1.add(anim, forKey: "transform")
CATransaction.commit()
}
func extendBar2(){
let transform1 = CATransform3DMakeScale(2, 1, 1)
let anim = CABasicAnimation(keyPath: "transform")
anim.isRemovedOnCompletion = false
anim.fillMode = kCAFillModeForwards
anim.toValue = NSValue(caTransform3D:transform1)
anim.duration = 5.00
progressBar1.add(anim, forKey: "transform")
}
Because you are modifying the transform property of the layer in both animation, it will be easier here to use CAKeyframeAnimation, which will handle the chaining of the animations for you.
func extendBar(){
let transform1 = CATransform3DMakeScale(10, 1, 1)
let transform2 = CATransform3DMakeScale(2, 1, 1)
let anim = CAKeyframeAnimation()
anim.keyPath = "transform"
anim.values = [progressBar1.transform, transform1, transform2] // the stages of the animation
anim.keyTimes = [0, 0.5, 1] // when they occurs, 0 being the very begining, 1 the end
anim.duration = 10.00
progressBar1.add(anim, forKey: "transform")
progressBar1.transform = transform2 // we set the transform property to the final animation's value
}
A word about the content of values and keyTimes:
We set the first value to be the current transform of progressBar1. This will make sure we start in the current layer's state.
In keyTimes, we say that at the begining, the first value in the values array should be used. We then say at animation's half time, the layer should be transformed in values second value. Hence, the animation between the initial state and the second one will occurs during that time. Then, between 0.5 to 1, we'll go from tranform1 to transform2.
You can read more about animations in this very nice article from objc.io.
If you really need two distinct animations (because maybe you want to add subviews to progressBar1 in between, here's the code that will do it
func extendBar1() {
CATransaction.begin()
CATransaction.setCompletionBlock {
print("side effects")
extendBar2()
}
let transform1 = CATransform3DMakeScale(5, 1, 1)
let anim = CABasicAnimation(keyPath: "transform")
anim.fromValue = progressBar1.transform
anim.toValue = transform1
anim.duration = 2.00
progressBar1.add(anim, forKey: "transform")
CATransaction.setDisableActions(true)
progressBar1.transform = transform1
CATransaction.commit()
}
func extendBar2() {
CATransaction.begin()
let transform2 = CATransform3DMakeScale(2, 1, 1)
let anim = CABasicAnimation(keyPath: "transform")
anim.fromValue = progressBar1.transform
anim.toValue = transform2
anim.duration = 2.00
progressBar1.add(anim, forKey: "transform")
CATransaction.setDisableActions(true)
progressBar1.transform = transform2
CATransaction.commit()
}
What happens here? Basically, we setup a first "normal animation". So we create the animation, that will modify the presentation layer and set the actual layer to the final first animation's transform.
Then, when the first animation completes, we call extendBar2 which will in turn queue a normal animation.
You also want to call CATransaction.setDisableActions(true) before explicitly updating the transform, otherwise, core animation will create an implicit one, which will override the one created just before.

CAShapeLayer detect touch during animation Swift

I can detect a touch of a CAShapeLayer like this (touchesEnded):
let touchLocation : CGPoint = (touch as! UITouch).locationInView(self.view)
for shape in shapes{
if CGPathContainsPoint(shape.path, nil, touchLocation, false){
print("Layer touch")
}
}
And I can animate the path of a CAShapeLayer like this:
let newShapePath = UIBezierPath(arcCenter: toPoint, radius: 20, startAngle: CGFloat(0), endAngle: CGFloat(M_PI * 2), clockwise: true).CGPath
// animate the `path`
let animation = CABasicAnimation(keyPath: "path")
animation.toValue = newShapePath
animation.duration = CFTimeInterval(duration)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
animation.fillMode = kCAFillModeBoth
animation.removedOnCompletion = false
shape.addAnimation(animation, forKey: animation.keyPath)
But while the animation is happening, touches aren't detected on the CAShapeLayer. Is it possible to detect a touch on the a CAShapeLayer while animating the path?
You can access the layer's presentationLayer in order to do this. This will provide you with a rough approximation to the 'in flight' values of a given layer while animating. For example:
for shape in shapes {
// gets the layer's presentation layer if it exists – else fallback on the model layer
let presentationLayer = shape.presentationLayer() as? CAShapeLayer ?? shape
if CGPathContainsPoint(presentationLayer.path, nil, touchLocation, false){
print("Layer touch")
}
}
Also, as a side note, it's generally considered bad practice to use removedOnCompletion = false if you're not using the animation delegate. Instead of leaving the animation lingering, you should just update the layer's model values to represent its new state. You can do this through a CATransaction to ensure that no implicit animations are generated. For example:
let animation = CABasicAnimation(keyPath: "path")
animation.fromValue = shape.path
animation.toValue = newShapePath
animation.duration = CFTimeInterval(duration)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
shape.addAnimation(animation, forKey: animation.keyPath)
// update the layer's model values
CATransaction.begin()
CATransaction.setDisableActions(true)
shape.path = newShapePath
CATransaction.commit()

Animate from current position in CAAnimation

How do I animate position of red ball from A to B starting from C ?
i.e. It should first animate from C to B and then animate indefinately between A & B
I have following code snippet
animation.fromValue = A
animation.toValue = B
animation.removedOnCompletion = false
animation.autoreverses = true
animation.repeatCount = Float.infinity
animation.duration = 2.0
The problem with this code snippet is that resets the position to A before starting the animation.
I want the animation to start from C to B and then animate between A and C
you will probably need to use 2 different animations: One from position C to position B, and then a repeating, auto-reversing animation from B back to A. If you can use it, UIView animations are much easier to use than CAAnimations. I'd try UIView animations first before diving into CAAnimation objects.
You can try to do something like:
UIView.animateWithDuration(0.2, animations: { () -> Void in
var frame = self.circleView.frame
frame.origin = A
self.circleView.frame = frame
}) { (finished) -> Void in
let animation = CABasicAnimation(keyPath: "position")
animation.fromValue = NSValue(CGPoint: CGPoint(x: A, y: self.circleView.frame.origin.y))
animation.toValue = NSValue(CGPoint: CGPoint(x: B, y: self.circleView.frame.origin.y))
animation.removedOnCompletion = false
animation.repeatCount = Float.infinity
animation.duration = 2.0
self.circleView.layer.addAnimation(animation, forKey: "position")
}
If you want to have it reversible instead of jumping to A point whenever it reaches to B, you can also add animation.autoreverses = true

Resources