I have a CAGradientLayer overlaid on a CALayer. I want to animate it's opacity as I pan around the view they reside in.
So I simply have a CAGradientLayer instantiated like so:
this is how I instantiate the layer:
CAGradientLayer * gLayer = [CAGradientLayer layer];
NSArray * stops;
NSNumber *stopOne = [NSNumber numberWithFloat:0.0];
NSNumber *stopTwo = [NSNumber numberWithFloat:1.0];
[gLayer setColors:#[(id)clearColor.CGColor,(id)darkColor.CGColor]];
[gLayer setLocations:stops];
Then I change the opacity during the gesture:
gLayer.opacity = (here I put the variable that changes between 0 - 1 as the pan changes)
So, even though this works, the change is not smooth because I am not animating it I guess.
How can I animate the opacity to be a fluid change? I guess the catch is that the animation has to be fast so that it follows the pan gesture change without lagging.
Any suggestion?
Thank you
The change will be fluid if you make an appropriate small change on every change in the gesture. The problem, in fact, is just the opposite of what you suspect: it is that the opacity is being animated, implicitly, but you keep canceling that animation because you do it again and again. Thus the change in opacity has no chance to become visible until the whole gesture is over. The solution, therefore, is to prevent the animation.
So each time your gesture recognizer handler is called, if the state is UIGestureRecognizerStateChanged, you are going to turn off implicit animation and then change the opacity of the layer, like this:
[CATransaction setDisableActions:YES];
gLayer.opacity = whatever;
Assuming that each whatever value is only slightly different from the previous whatever value, the result will be smooth. (But of course it is up to you to supply a sensible whatever value each time. I do not know whether you're doing that, as you have not revealed that part of your code.)
Related
I am using CABasicAnimation to move and resize an image view. I want the image view to be added to the superview, animate, and then be removed from the superview.
In order to achieve that I am listening for delegate call of my CAAnimationGroup, and as soon as it gets called I remove the image view from the superview.
The problem is that sometimes the image blinks in the initial location before being removed from the superview. What's the best way to avoid this behavior?
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = [NSArray arrayWithObjects:moveAnim, scaleAnim, opacityAnim, nil];
animGroup.duration = .5;
animGroup.delegate = self;
[imageView.layer addAnimation:animGroup forKey:nil];
When you add an animation to a layer, the animation does not change the layer's properties. Instead, the system creates a copy of the layer. The original layer is called the model layer, and the duplicate is called the presentation layer. The presentation layer's properties change as the animation progresses, but the model layer's properties stay unchanged.
When you remove the animation, the system destroys the presentation layer, leaving only the model layer, and the model layer's properties then control how the layer is drawn. So if the model layer's properties don't match the final animated values of the presentation layer's properties, the layer will instantly reset to its appearance before the animation.
To fix this, you need to set the model layer's properties to the final values of the animation, and then add the animation to the layer. You want to do it in this order because changing a layer property can add an implicit animation for the property, which would conflict with the animation you want to explicitly add. You want to make sure your explicit animation overrides the implicit animation.
So how do you do all this? The basic recipe looks like this:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.fromValue = [NSValue valueWithCGPoint:myLayer.position];
layer.position = newPosition; // HERE I UPDATE THE MODEL LAYER'S PROPERTY
animation.toValue = [NSValue valueWithCGPoint:myLayer.position];
animation.duration = .5;
[myLayer addAnimation:animation forKey:animation.keyPath];
I haven't used an animation group so I don't know exactly what you might need to change. I just add each animation separately to the layer.
I also find it easier to use the +[CATransaction setCompletionBlock:] method to set a completion handler for one or several animations, instead of trying to use an animation's delegate. You set the transaction's completion block, then add the animations:
[CATransaction begin]; {
[CATransaction setCompletionBlock:^{
[self.imageView removeFromSuperview];
}];
[self addPositionAnimation];
[self addScaleAnimation];
[self addOpacityAnimation];
} [CATransaction commit];
CAAnimations are removed automatically when complete. There is a property removedOnCompletion that controls this. You should set that to NO.
Additionally, there is something known as the fillMode which controls the animation's behavior before and after its duration. This is a property declared on CAMediaTiming (which CAAnimation conforms to). You should set this to kCAFillModeForwards.
With both of these changes the animation should persist after it's complete. However, I don't know if you need to change these on the group, or on the individual animations within the group, or both.
Heres an example in Swift that may help someone
It's an animation on a gradient layer. It's animating the .locations property.
The critical point as #robMayoff answer explains fully is that:
Surprisingly, when you do a layer animation, you actually set the final value, first, before you start the animation!
The following is a good example because the animation repeats endlessly.
When the animation repeats endlessly, you will see occasionally a "flash" between animations, if you make the classic mistake of "forgetting to set the value before you animate it!"
var previousLocations: [NSNumber] = []
...
func flexTheColors() { // "flex" the color bands randomly
let oldValues = previousTargetLocations
let newValues = randomLocations()
previousTargetLocations = newValues
// IN FACT, ACTUALLY "SET THE VALUES, BEFORE ANIMATING!"
theLayer.locations = newValues
// AND NOW ANIMATE:
CATransaction.begin()
// and by the way, this is how you endlessly animate:
CATransaction.setCompletionBlock{ [weak self] in
if self == nil { return }
self?.animeFlexColorsEndless()
}
let a = CABasicAnimation(keyPath: "locations")
a.isCumulative = false
a.autoreverses = false
a.isRemovedOnCompletion = true
a.repeatCount = 0
a.fromValue = oldValues
a.toValue = newValues
a.duration = (2.0...4.0).random()
theLayer.add(a, forKey: nil)
CATransaction.commit()
}
The following may help clarify something for new programmers. Note that in my code I do this:
// IN FACT, ACTUALLY "SET THE VALUES, BEFORE ANIMATING!"
theLayer.locations = newValues
// AND NOW ANIMATE:
CATransaction.begin()
...set up the animation...
CATransaction.commit()
however in the code example in the other answer, it's like this:
CATransaction.begin()
...set up the animation...
// IN FACT, ACTUALLY "SET THE VALUES, BEFORE ANIMATING!"
theLayer.locations = newValues
CATransaction.commit()
Regarding the position of the line of code where you "set the values, before animating!" ..
It's actually perfectly OK to have that line actually "inside" the begin-commit lines of code. So long as you do it before the .commit().
I only mention this as it may confuse new animators.
I'm implementing a custom pull-to-refresh component.
Create CALayer with a spinner animation, than add this layer to UICollectionView. Layer's speed is zero (self.loaderLayer.speed = 0.0f;) and layer animation is managed with timeOffset in scrollViewDidScroll:. Problem goes here, because I also want to show a loader always in the center of pulling space, so I change layer's position in scrollViewDidScroll: like this:
[self.loaderLayer setPosition:CGPointMake(CGRectGetMidX(scrollView.bounds), scrollView.contentOffset.y / 2)];
But nothing happens with layer position (calling [self.loaderLayer setNeedsDisplay] doesn't help). I understand that it's because zero speed. And currently I found the way which works (but I don't like that):
self.loaderLayer.speed = 1.0f;
[self.loaderLayer setPosition:CGPointMake(CGRectGetMidX(scrollView.bounds), scrollView.contentOffset.y / 2)];
self.loaderLayer.speed = 0.0f;
How could I change a position for a paused layer right? What am I missing?
All regards to #David for the reference. I just summarize it as an answer.
CoreAnimation works with two kinds of animations (transactions): explicit and implicit. When you see Animatable word in property documentation, it means that each time you set this property, CoreAnimation will animate this property changes implicitly with system default duration (default is 1/4 second). Under hood CALayer has actions for these properties and calling -actionForKey returns such action (implicit animation).
So in my case, when I change a layer position, CoreAnimation implicitly try animating this changes. Because layer is paused (speed is zero) and animation has default duration, we don't see this changes visually.
And answer is to disable implicit animations (disable calling layer -actionForKey). To do that we call [CATransaction setDisableActions:YES].
OR
We can mark this animation as immediate (by setting it's duration to zero) with calling [CATransaction setAnimationDuration:0.0];.
These flags/changes are per thread based and work for all transactions in specific thread until next run loop. So if we want to apply them for a concrete transaction, we wrap code with [CATransaction begin]; ... [CATransaction commit]; section.
In my case final code looks
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self.loaderLayer setPosition:CGPointMake(CGRectGetMidX(scrollView.bounds), scrollView.contentOffset.y / 2)];
self.loaderLayer.transform = CATransform3DMakeScale(scaleFactor, scaleFactor, 1);
[CATransaction commit];
And it works perfectly!
I trying to find a way for connecting user interaction (pan gesture) with animation. I want to control animation progress by dragging view (or just by finger panning on the screen).
I think I can make this by using Core Animation, but I didn't find any similar to my needs example.
I have a CGPath, that I want to draw.
I know that there are strokeStart and strokeEnd property that is animatable and perfectly acceptable for my needs in the "drawing" point of view.
Using layer with given path property I can animate this path with CABasicAnimation
layer.path = myPath
//... layer configuration
let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = 2
pathAnimation.fromValue = 0
pathAnimation.toValue = 1
layer.addAnimation(pathAnimation, forKey: "strokeEndAnimation")
Here we can see animation from the first path point to the last consequentially.
Okay, but I need to somehow control this animation with gesture.
Let's say, if we pan finger on the screen to the right, our path animation draws straight ahead (e.g strokeStart = 0 and strokeEnd = 1) and if we pan from right to left we will see backward animation (like rewind) (e.g. strokeStart = 1 and strokeEnd = 0).
I know, how to get gesture direction, translationInView, velocityInView and I want to animate this in changed state, not end. (UIGestureRecognizerState.Changed)
But I don't know how I can combine control from gesture to animation because of this troubles:
duration of animation. (I don't know how fast or how slow gesture would be)
how I need to implement animation because calls are very frequent in changed state
I looked to
animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:
but duration question rises again.
Hope somebody can help me, ask me if you misunderstood something and I'll try to clarify.
Let's say, if we pan finger on the screen to the right, our path animation draws straight ahead (e.g strokeStart = 0 and strokeEnd = 1) and if we pan from right to left we will see backward animation (like rewind) (e.g. strokeStart = 1 and strokeEnd = 0).
If I understand you correctly, the solution is to freeze the animation by setting the layer's speed to 0 and then setting the "frame" of the animation by setting the layer's timeOffset.
I am trying to delay the animation of layer's opacity and position by 3 seconds using setBeginTime. I have called the layer boxLayer. The animation is going well however during the first 3 seconds (the layer is not supposed to show yet) the layer is displayed at its final position and opacity. It should not. Group animation does not resolve the issue. Could anyone help? See code below:
// Create an animation that will change the opacity of a layer
CABasicAnimation *fader = [CABasicAnimation animationWithKeyPath:#"opacity"];
// It will last 1 second and will be delayed by 3 seconds
[fader setDuration:1.0];
[fader setBeginTime:CACurrentMediaTime()+3.0];
// The layer's opacity will start at 0.0 (completely transparent)
[fader setFromValue:[NSNumber numberWithFloat:startOpacity]];
// And the layer will end at 1.0 (completely opaque)
[fader setToValue:[NSNumber numberWithFloat:endOpacity]];
// Add it to the layer
[boxLayer addAnimation:fader forKey:#"BigFade"];
// Maintain opacity to 1.0 JUST TO MAKE SURE IT DOES NOT GO BACK TO ORIGINAL OPACITY
[boxLayer setOpacity:endOpacity];
// Create an animation that will change the position of a layer
CABasicAnimation *mover = [CABasicAnimation animationWithKeyPath:#"position"];
// It will last 1 second and will be delayed by 3 seconds
[mover setDuration:1.0];
[mover setBeginTime:CACurrentMediaTime()+3.0];
// Setting starting position
[mover setFromValue:[NSValue valueWithCGPoint:CGPointMake(startX, startY)]];
// Setting ending position
[mover setToValue:[NSValue valueWithCGPoint:CGPointMake(endX, endY)]];
// Add it to the layer
[boxLayer addAnimation:mover forKey:#"BigMove"];
// Maintain the end position at 400.0 450.0 OTHERWISE IT IS GOING BACK TO ORIGINAL POSITION
[boxLayer setPosition:CGPointMake(endX, endY)];
The problem is that you're setting the boxLayer properties of position and of opacity to their end values. You need to:
Set the boxLayer properties to their starting values, not their ending values (this is why it's starting in the ending position/opacity ... usually if the animation starts immediately, this isn't an issue, but because you're deferring the start, using the ending positions is problematic);
For your two animations, you have to change removedOnCompletion to NO and fillMode to kCAFillModeForwards (this is the correct way to keep it from reverting back to the original position upon completion).
Thus:
// Create an animation that will change the opacity of a layer
CABasicAnimation *fader = [CABasicAnimation animationWithKeyPath:#"opacity"];
// It will last 1 second and will be delayed by 3 seconds
[fader setDuration:1.0];
[fader setBeginTime:CACurrentMediaTime()+3.0];
// The layer's opacity will start at 0.0 (completely transparent)
[fader setFromValue:[NSNumber numberWithFloat:startOpacity]];
// And the layer will end at 1.0 (completely opaque)
[fader setToValue:[NSNumber numberWithFloat:endOpacity]];
// MAKE SURE IT DOESN'T CHANGE OPACITY BACK TO STARTING VALUE
[fader setRemovedOnCompletion:NO];
[fader setFillMode:kCAFillModeForwards];
// Add it to the layer
[boxLayer addAnimation:fader forKey:#"BigFade"];
// SET THE OPACITY TO THE STARTING VALUE
[boxLayer setOpacity:startOpacity];
// Create an animation that will change the position of a layer
CABasicAnimation *mover = [CABasicAnimation animationWithKeyPath:#"position"];
// It will last 1 second and will be delayed by 3 seconds
[mover setDuration:1.0];
[mover setBeginTime:CACurrentMediaTime()+3.0];
// Setting starting position
[mover setFromValue:[NSValue valueWithCGPoint:CGPointMake(startX, startY)]];
// Setting ending position
[mover setToValue:[NSValue valueWithCGPoint:CGPointMake(endX, endY)]];
// MAKE SURE IT DOESN'T MOVE BACK TO STARTING POSITION
[mover setRemovedOnCompletion:NO];
[mover setFillMode:kCAFillModeForwards];
// Add it to the layer
[boxLayer addAnimation:mover forKey:#"BigMove"];
// SET THE POSITION TO THE STARTING POSITION
[boxLayer setPosition:CGPointMake(startX, startY)];
Personally, I think you're doing a lot of work for something that's done far more easily with block-based animation on the view (for the purposes of this demonstration, I'm assuming your boxLayer is a CALayer for a control called box). You don't need Quartz 2D, either, if you do it this way:
box.alpha = startOpacity;
box.frame = CGRectMake(startX, startY, box.frame.size.width, box.frame.size.height);
[UIView animateWithDuration:1.0
delay:3.0
options:0
animations:^{
box.alpha = endOpacity;
box.frame = CGRectMake(endX, endY, box.frame.size.width, box.frame.size.height);
}
completion:nil];
For using beginTime you should make necessary configuration of your animation object and set fillMode to kCAFillModeBackwards like
zoomAnimation.fillMode = kCAFillModeBackwards;
That's said in Apple documentation:
Use the beginTime property to set the start time of an animation. Normally, animations begin during the next update cycle. You can use the beginTime parameter to delay the animation start time by several seconds. The way to chain two animations together is to set the begin time of one animation to match the end time of the other animation.
If you delay the start of an animation, you might also want to set the fillMode property to kCAFillModeBackwards. This fill mode causes the layer to display the animation’s start value, even if the layer object in the layer tree contains a different value. Without this fill mode, you would see a jump to the final value before the animation starts executing. Other fill modes are available too.
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html#//apple_ref/doc/uid/TP40004514-CH8-SW2
Also, from Rob's answer:
For your two animations, you have to change removedOnCompletion to NO and fillMode to kCAFillModeForwards (this is the correct way to keep it from reverting back to the original position upon completion).
It's a kind of controversial statement, because:
Once the animation is removed, the presentation layer will fall back to the values of the model layer, and since we’ve never modified that layer’s position, our spaceship reappears right where it started.
There are two ways to deal with this issue:
The first approach is to update the property directly on the model layer. This is the recommended approach, since it makes the animation completely optional.
Alternatively, you can tell the animation to remain in its final state by setting its fillMode property to kCAFillModeForwards and prevent it from being automatically removed by setting removedOnCompletion to NO. However, it’s a good practice to keep the model and presentation layers in sync, so this approach should be used carefully.
From https://www.objc.io/issues/12-animations/animations-explained/
This article explains well why you shouldn't use removedOnCompletion with fillMode https://www.objc.io/issues/12-animations/animations-explained/
In my case I'm animating the layer of a view that functions as a navigation but delaying a bounce animation that is inside that view ; I NEED BOTH OF THESE POSITIONS UPDATED ON THE LAYER since it can be dismissed and then shown again. Using removedOnCompletion will not update the layer's value once the animation completes
The way I do it is update the layer in a CATransaction completion block
CATransaction.setCompletionBlock {
// update the layer value
}
CATransaction.begin()
// setup and add your animation
CATransaction.commit()
I need a a little help with my animation.
I got two buttons, the first button calls an animation and the second one calls another animation.
Now when I press the first button a code like this one down below will be called.
The Problem is that when I press the second button the animation kinda teleports to it's starting position (second animations position), is there a way to make the animations flow together from where it was interupted?
CABasicAnimation *anim1 = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
anim1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
anim1.fromValue = [NSNumber numberWithFloat:((-10*M_PI)/180)];
anim1.toValue = [NSNumber numberWithFloat:((10*M_PI)/180)];
anim1.repeatCount = HUGE_VALF;
anim1.autoreverses = YES;
anim1.duration = .5;
[head addAnimation:anim1 forKey:#"transform"];
Virtual picture:
http://i47.tinypic.com/2yopif6.jpg
The black line is the first animation, the red is the second animation, and the green is the one I'm trying to figure out. According to the picture it was interupted in the middle of the black.
Why not using the animation method instead of the UIView class - just make sure you use the UIViewAnimationOptionBeginFromCurrentState option?
Check the reference doc here: http://developer.apple.com/library/ios/#documentation/uikit/reference/uiview_class/uiview/uiview.html
EDIT
I think this tutorial will help you better: http://wangling.me/2011/06/core-animation-101-from-and-to/. What they are trying to achieve there is to bounce a ball after triggering the animation through a button. When the button is pressed a second time the ball should bounce back from the current position. There is a section where they mention they rely on 2 delegates method animationDidStart and animationDidStop to implement this desired effect.