Xcode: how to do linear animation in a CATransaction - ios

My transaction starts slow, gets fast, then goes slow.
I need linear speed throughout.
I've found UIViewAnimationOptionCurveLinear but can't find an example of [CATransaction begin]
Here is my code:
[ CATransaction begin ];
if( graph_animation_enable )
[CATransaction setAnimationDuration: graph_animation_seconds ];
else
[CATransaction setAnimationDuration: 0 ];
//[CATransaction setValue : ( id ) kCFBooleanTrue forKey : kCATransactionDisableActions];
graph_CALayer.frame = CGRectMake( left_x, top_y, width, height );
graph_CALayer.backgroundColor = bar_background_color.CGColor;
CAMediaTimingFunction *linearTiming =
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionLinear];
[CATransaction setAnimationTimingFunction: linearTiming];
[CATransaction commit];
I TRIED THE ANSWER, BUT STILL NON-LINEAR.
Animation starts, but then slows down.

Use CATransaction setAnimationTimingFunction and the timing value of kCAMediaTimingFunctionLinear
The code would look like this:
CAMediaTimingFunction *linearTiming =
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionLinear];
[CATransaction setAnimationTimingFunction: linearTiming]

With Swift 5, CATransaction has a method called setAnimationTimingFunction(_:). setAnimationTimingFunction(_:) has the following declaration:
class func setAnimationTimingFunction(_ function: CAMediaTimingFunction?)
Sets the timing function used for all animations within this transaction group. [...] This is a convenience method that sets the CAMediaTimingFunction for the value(forKey:) value of the kCATransactionAnimationTimingFunction key.
Therefore, you can use one of the following code snippets in order to set a linear animation for your CATransaction:
CATransaction.begin()
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear))
/* ... */
CATransaction.commit()
CATransaction.begin()
CATransaction.setValue(CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear), forKey: kCATransactionAnimationTimingFunction)
/* ... */
CATransaction.commit()

Related

Core Animation short hand causes odd behaviour [duplicate]

Here's some relevant code inside a UIView subclass:
- (void) doMyCoolAnimation {
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.duration = 4;
[self.layer setValue:#200 forKeyPath:anim.keyPath];
[self.layer addAnimation:anim forKey:nil];
}
- (CGFloat) currentX {
CALayer* presLayer = self.layer.presentationLayer;
return presLayer.position.x;
}
When I use [self currentX] while the animation is running, I get 200 (the end value) rather than a value between 0 (the start value) and 200. And yes, the animation is visible to the user, so I'm really confused here.
Here's the code where I call doMyCoolAnimation:, as well as currentX after 1 second.
[self doMyCoolAnimation];
CGFloat delay = 1; // 1 second delay
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
NSLog(#"%f", [self currentX]);
});
Any ideas?
I don't know where the idea for using KVC setters in animation code came from, but that's what the animation itself is for. You're basically telling the layer tree to immediately update to the new position with this line:
[self.layer setValue:#200 forKeyPath:anim.keyPath];
Then wondering why the layer tree won't animate to that position with an animation that has no starting or ending values. There's nothing to animate! Set the animation's toValue and fromValue as appropriate and ditch the setter. Or, if you wish to use an implicit animation, keep the setter, but ditch the animation and set its duration by altering the layer's speed.
My UIView's layer's presentationLayer was not giving me the current values. It was instead giving me the end values of my animation.
To fix this, all I had to do was add...
anim.fromValue = [self.layer valueForKeyPath:#"position.x"];
...to my doMyCoolAnimation method BEFORE I set the end value with:
[self.layer setValue:#200 forKeyPath:#"position.x"];
So in the end, doMyCoolAnimation looks like this:
- (void) doMyCoolAnimation {
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.duration = 4;
anim.fromValue = [self.layer valueForKeyPath:anim.keyPath];
[self.layer setValue:#200 forKeyPath:anim.keyPath];
[self.layer addAnimation:anim forKey:nil];
}
The way you are creating your animation is wrong, as CodaFi says.
Either use an explicit animation, using a CABasicAnimation, or use implicit animation by changing the layer's properties directly and NOT using a CAAnimation object. Don't mix the two.
When you create a CABasicAnimation object, you use setFromValue and/or setToValue on the animation. Then the animation object takes care of animating the property in the presentation layer.

How to properly set the model when doing CABeginAnimation

I have a UIImageView that when the user taps it, a border of 4 points toggles on and off. I'm trying to animate the border in and out as follows:
CABasicAnimation *widthAnimation = [CABasicAnimation animationWithKeyPath:#"borderWidth"];
widthAnimation.toValue = self.isSelected ? #4.0 : #0.0;
widthAnimation.duration = 0.1;
[self.imageView.layer addAnimation:widthAnimation forKey:#"borderWidth"];
Now, as I've learned from research and scouring SO, CABasicAnimation just changes the presentation layer, but not the actual model. I've also read that using fillMode and removedOnCompletion is bad practice, since it leads to inconsistencies between the model and what the user sees. So, I tried to change the model with the following line:
self.imageView.layer.borderWidth = self.isSelected ? 4.0 : 0.0;
The problem is, this line seems to set the property straight away, so by the time the animation kicks in, the border width is already at it's desired value. I've tried sticking this line at the beginning of the code, end, and everywhere in between, but to no success. I did manage to find a hacky solution: instead of setting the property, I passed the property setter to performSelector: withObject: afterDelay:, with the delay being the duration of the animation. This works most of the time, but sometimes the cycles don't quite match up, and the animation will run first, then it jumps back to the original state, then it snaps to the new state, presumably as a result of performSelector
So is there any way to smoothly animate a border without performSelector?
Any help is greatly appreciated.
Here is an example of CABasicAnimation I made a while ago :
-(void) animateProgressFrom:(CGFloat)fromValue to:(CGFloat)toValue
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"opacity"];
animation.fromValue = #(fromValue);
animation.toValue = #(toValue);
animation.duration = ABS(toValue - fromValue)*3.0;
[self.layer addAnimation:animation forKey:#"opacity"];
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.layer.opacity = toValue;
[CATransaction commit];
}
I think what you needed is the CATransaction at the end of the layer animation.

UIView.layer.presentationLayer returns final value (rather than current value)

Here's some relevant code inside a UIView subclass:
- (void) doMyCoolAnimation {
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.duration = 4;
[self.layer setValue:#200 forKeyPath:anim.keyPath];
[self.layer addAnimation:anim forKey:nil];
}
- (CGFloat) currentX {
CALayer* presLayer = self.layer.presentationLayer;
return presLayer.position.x;
}
When I use [self currentX] while the animation is running, I get 200 (the end value) rather than a value between 0 (the start value) and 200. And yes, the animation is visible to the user, so I'm really confused here.
Here's the code where I call doMyCoolAnimation:, as well as currentX after 1 second.
[self doMyCoolAnimation];
CGFloat delay = 1; // 1 second delay
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
NSLog(#"%f", [self currentX]);
});
Any ideas?
I don't know where the idea for using KVC setters in animation code came from, but that's what the animation itself is for. You're basically telling the layer tree to immediately update to the new position with this line:
[self.layer setValue:#200 forKeyPath:anim.keyPath];
Then wondering why the layer tree won't animate to that position with an animation that has no starting or ending values. There's nothing to animate! Set the animation's toValue and fromValue as appropriate and ditch the setter. Or, if you wish to use an implicit animation, keep the setter, but ditch the animation and set its duration by altering the layer's speed.
My UIView's layer's presentationLayer was not giving me the current values. It was instead giving me the end values of my animation.
To fix this, all I had to do was add...
anim.fromValue = [self.layer valueForKeyPath:#"position.x"];
...to my doMyCoolAnimation method BEFORE I set the end value with:
[self.layer setValue:#200 forKeyPath:#"position.x"];
So in the end, doMyCoolAnimation looks like this:
- (void) doMyCoolAnimation {
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.duration = 4;
anim.fromValue = [self.layer valueForKeyPath:anim.keyPath];
[self.layer setValue:#200 forKeyPath:anim.keyPath];
[self.layer addAnimation:anim forKey:nil];
}
The way you are creating your animation is wrong, as CodaFi says.
Either use an explicit animation, using a CABasicAnimation, or use implicit animation by changing the layer's properties directly and NOT using a CAAnimation object. Don't mix the two.
When you create a CABasicAnimation object, you use setFromValue and/or setToValue on the animation. Then the animation object takes care of animating the property in the presentation layer.

How do I chain a sequence of animations of the same property on three different objects?

I have been trying to create a sequence of animations that deal three cards from a poker deck sequentially. I just want to animate the position of the three cards -- say the first animation begins immediately on the first card's position and goes for 0.4 seconds, the second begins after 0.4 seconds with the same duration, and the last begins after 0.8 seconds. I can't figure out how to do this! The code below doesn't work. Perhaps I need to use a CAGroupAnimation, but I don't know how to make a group of sequential
animations on the same property!
CGFloat beginTime = 0.0;
for (Card *c in cards) {
CardLayer *cardLayer = [cardToLayerDictionary objectForKey:c];
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"position"];
anim.fromValue = [NSValue valueWithCGPoint:stockLayer.position];
anim.toValue = [NSValue valueWithCGPoint:wasteLayer.position];
anim.duration = 0.4;
anim.beginTime = beginTime;
beginTime += 0.4;
cardLayer.position = wasteLayer.position;
[cardLayer addAnimation:anim forKey:#"position"];
…
}
Like Einstein said, time is relative. All your layers' beginTimes are relative to the timespace of their superlayer---since it isn't animating, they all end up the same.
It looks like there are two possible solutions:
Get an absolute timebase and set each layer's animation's beginTime relative to it:
int i = 0; float delay = 0.4;
for (Card *c in cards) {
// ...
float baseTime = [cardLayer convertTime:CACurrentMediaTime() fromLayer:nil];
anim.beginTime = baseTime + (delay * i++);
// ...
}
Wrap each card's animation in a group. For some reason I don't quite get, this puts those animations on a common timescale, even though these groups are separate from one another. It's worked for some, but I'm a bit disinclined to trust it---seems like spooky action at a distance.
Either way, you're probably also going to want the layers to stay where they are at the end of the animation. You could make the animation "stick" like so:
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;
But that might give your trouble later---the layer's model position is still where it was to start, and if you adjust that after the animation (say, for dragging a card around) you might get weird results. So it might be appropriate to wrap your card dealing animation in a CATransaction, on which you set a completion block that sets the layers' final positions.
Speaking of CATransactions with completion blocks, you could use those to make implicit animations happen in sequence (if you're not using explicit animation for some other reason):
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[CATransaction begin];
[CATransaction setCompletionBlock:^{
card3Layer.position = wasteLayer.position;
}];
card2Layer.position = wasteLayer.position;
[CATransaction end];
}];
card1Layer.position = wasteLayer.position;
[CATransaction end];
The following recursive method is the best I could do to chain animations. I use a CATransaction to animate the position property and set up a block that makes a recursive call for the next card.
-(void)animateDeal:(NSMutableArray*)cardLayers {
if ([cardLayers count] > 0) {
CardLayer *cardLayer = [cardLayers objectAtIndex:0];
[cardLayers removeObjectAtIndex:0];
// ...set new cardLayer.zPosition with animations disabled...
[CATransaction begin];
[CATransaction setCompletionBlock:^{[self animateDeal:cardLayers];}];
[CATransaction setAnimationDuration:0.25];
[cardLayer setFaceUp:YES];
cardLayer.position = wasteLayer.position;
[CATransaction commit];
}
}
Of course this doesn't solve the chaining problem with overlapping time intervals.

How do you move a CALayer instantly (without animation)

I'm trying to drag a CALayer in an iOS app.
As soon as I change its position property it tries to animate to the new position and flickers all over the place:
layer.position = CGPointMake(x, y)
How can I move CALayers instantly? I can't seem to get my head around the Core Animation API.
You want to wrap your call in the following:
[CATransaction begin];
[CATransaction setValue: (id) kCFBooleanTrue forKey: kCATransactionDisableActions];
layer.position = CGPointMake(x, y);
[CATransaction commit];
Swift 3 Extension :
extension CALayer {
class func performWithoutAnimation(_ actionsWithoutAnimation: () -> Void){
CATransaction.begin()
CATransaction.setValue(true, forKey: kCATransactionDisableActions)
actionsWithoutAnimation()
CATransaction.commit()
}
}
Usage :
CALayer.performWithoutAnimation(){
someLayer.position = newPosition
}
You can also use the convenience function
[CATransaction setDisableActions:YES]
as well.
Note: Be sure to read the comments by Yogev Shelly to understand any gotchas that could occur.
As others have suggested, you can use CATransaction.
The problem comes arises because CALayer has a default implicit animation duration of 0.25 seconds.
Thus, an easier (in my opinion) alternative to setDisableActions is to use setAnimationDuration with a value of 0.0.
[CATransaction begin];
[CATransaction setAnimationDuration:0.0];
layer.position = CGPointMake(x, y);
[CATransaction commit];
Combining previous answers here for Swift 4, to clearly make the animation duration explicit...
extension CALayer
{
class func perform(withDuration duration: Double, actions: () -> Void) {
CATransaction.begin()
CATransaction.setAnimationDuration(duration)
actions()
CATransaction.commit()
}
}
Usage...
CALayer.perform(withDuration: 0.0) {
aLayer.frame = aFrame
}

Resources