I'm building an app that will have an animation of one image "walking" across the screen, using simple CABasicAnimation. I have it set for it to walk a certain distance in a duration, and then stop until the user gives it more commands, in which it will continue walking for the same distance and duration again. The issue that I have is that after the first time, the image will stop where it should, but it won't continue to walk, it will jump back to its original spot and start over. I thought I had this set correctly on the origin point, but I guess not.
CABasicAnimation *hover = [CABasicAnimation animationWithKeyPath:#"position"];
hover.fillMode = kCAFillModeForwards;
hover.removedOnCompletion = NO;
hover.additive = YES; // fromValue and toValue will be relative instead of absolute values
hover.fromValue = [NSValue valueWithCGPoint:CGPointZero];
hover.toValue = [NSValue valueWithCGPoint:CGPointMake(110.0, -50.0)]; // y increases downwards on iOS
hover.autoreverses = FALSE; // Animate back to normal afterwards
hover.duration = 10.0; // The duration for one part of the animation (0.2 up and 0.2 down)
hover.repeatCount = 0; // The number of times the animation should repeat
[theDude.layer addAnimation:hover forKey:#"myHoverAnimation"];
Your from value is set to be zero and it's not being updated.
hover.fromValue = [NSValue valueWithCGPoint:CGPointZero];
You have to update this value with your to value each time.
Here I've put your code in a function where you can update starting and ending points.
- (void)moveFromPoint:(CGPoint)fromPoint toPoint:(CGPoint)toPoint {
CABasicAnimation *hover = [CABasicAnimation animationWithKeyPath:#"position"];
hover.fillMode = kCAFillModeForwards;
hover.removedOnCompletion = NO;
hover.additive = YES; // fromValue and toValue will be relative instead of absolute values
hover.fromValue = [NSValue valueWithCGPoint:fromPoint];
hover.toValue = [NSValue valueWithCGPoint:toPoint]; // y increases downwards on iOS
hover.autoreverses = FALSE; // Animate back to normal afterwards
hover.duration = 10.0; // The duration for one part of the animation (0.2 up and 0.2 down)
hover.repeatCount = 0; // The number of times the animation should repeat
[theDude.layer addAnimation:hover forKey:#"myHoverAnimation"];
}
You can move the guy further by calling this function with new points each time.
[self moveFromPoint:CGPointZero toPoint:CGPointMake(110.0, -50.0)]
[self moveFromPoint:CGPointMake(110.0, -50.0) toPoint:CGPointMake(160.0, -50.0)]
EDIT:
I see that you want to move the guy with the same ratio but in different lenght each time.
Add this variable just after the #interface:
#property (nonatomic) CGPoint oldPointOfTheGuy;
And add this new function just after the previous function:
- (void)moveByDistance:(CGFloat)distance {
CGPoint newPointOfTheGuy = CGPointMake(self.oldPointOfTheGuy.x + 2.2*distance, self.oldPointOfTheGuy.y + distance);
[self moveFromPoint:self.oldPointOfTheGuy toPoint:newPointOfTheGuy];
self.oldPointOfTheGuy = newPointOfTheGuy;
}
And set a starting point for the guy in your viewDidLoad:
self.oldPointOfTheGuy = CGPointMake(110.0, -50)
Now we have set the old position of the guy as where we know he spawns for the first time.
From now on, each time we want to move him, we will call this:
[self moveByDistance:20];
What this function does is, since that it already knows your x / y ratio which is 2.2, it just adds 20 to your old y position and adds 2.2 * 20 to your old x position. And each time a new position is set, old position is updated.
Hope this helps.
Related
I have one this scenario:
I add to a layer CAAnimation that transforms it to a specific frame. with a starting time of 0.
Then I add another CAAnimation that transforms it to a different frame. with a starting time of 0.5.
what happens is that the layer immediately gets the second frame (with no animation) and after the first animation time passes the second animation is completed correctly.
This is the animation creation code:
+ (CAAnimation *)transformAnimation:(CALayer *)layer
fromFrame:(CGRect)fromFrame
toFrame:(CGRect)toFrame
fromAngle:(CGFloat)fromAngle
toAngle:(CGFloat)toAngle
anchor:(CGPoint)anchor
vertical:(BOOL)vertical
begin:(CFTimeInterval)begin
duration:(CFTimeInterval)duration {
CATransform3D fromTransform = makeTransform(layer, fromFrame, fromAngle, anchor, vertical);
CATransform3D midTransform1 = makeTransformLerp(layer, fromFrame, toFrame, fromAngle, toAngle, anchor, 0.33, vertical);
CATransform3D midTransform2 = makeTransformLerp(layer, fromFrame, toFrame, fromAngle, toAngle, anchor, 0.66, vertical);
CATransform3D toTransform = makeTransform(layer, toFrame, toAngle, anchor, vertical);
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
animation.values = #[[NSValue valueWithCATransform3D:fromTransform],
[NSValue valueWithCATransform3D:midTransform1],
[NSValue valueWithCATransform3D:midTransform2],
[NSValue valueWithCATransform3D:toTransform]
];
animation.beginTime = begin;
animation.duration = duration;
animation.fillMode = kCAFillModeBoth;
animation.calculationMode = kCAAnimationPaced;
animation.removedOnCompletion = NO;
return animation;
}
EDIT
in most scenarios, this code works well and the animations are sequenced correctly. But if I set 1 transform animation to start after 2 seconds and then set another transform to start after 4 seconds. the first transform is applied immediately to the layer and the second animation starts from there.
Any Idea how can I separate the animation to run one after the other?
(I prefer not using a completion block)
Thanks
The easiest and most glaring early fix would be to change the fill mode so that the second animation is not clamped on both ends overriding the previous animation.
animation.fillMode = kCAFillModeForwards;
Also I would adjust the begin time to be
animation.beginTime = CACurrentMediaTime() + begin;
If it is a matter of overlapping begin times and durations and not this let me know and I can provide that as well.
Context:
I have a graphic that I rotate continuously while a background operation happens:
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = #(M_PI * 2.0);
rotationAnimation.duration = 2;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = HUGE_VALF;
[myImgView.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
At some point this background animation ends, and I stop the animation:
[myImgView.layer removeAllAnimations];
When I do this, though, the rotation stops wherever it is and snaps back to zero rotation. I could just stop it where it is by promoting the presentation value to the model value:
CATransform3D stoppedTransform = myImgView.layer.presentationLayer.transform;
[myImgView.layer removeAllAnimations];
myImgView.layer.transform = stoppedTransform;
This works OK. But ideally I'd like to continue the rotation for the rest of its current circuit, and decelerate and stop smoothly as it approaches and rests at zero rotation. Stop the animation, promote presentation to model, and create a new animation with an ease-out curve to zero. Something like this:
CATransform3D stoppedTransform = myImgView.layer.presentationLayer.transform;
[myImgView.layer removeAllAnimations];
myImgView.layer.transform = stoppedTransform;
CGFloat currRotation = [[myImgView.layer valueForKeyPath:#"transform.rotation.z"] floatValue];
CABasicAnimation *finishAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
finishAnimation.fromValue = #(currRotation);
finishAnimation.toValue = #(0);
finishAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
finishAnimation.duration = 2; //this is what I need to calculate correctly
[myImgView.layer setValue:#(0) forKeyPath:#"transform.rotation.z"];
[myImgView.layer addAnimation:finishAnimation forKey:#"transform.rotation.z"];
My question:
How do I calculate the duration of my finishing animation with its ease-out curve so that its initial velocity matches the velocity of the existing rotation (currently pi radians per second) so there's no visible hiccup? Or is there another way to achieve the desired effect?
I have tried using CASpringAnimation and using its initialVelocity property, but it seems to combine that with the velocity imparted by the spring pulling on the mass, which is governed by mass, stiffness, and damping, and its units are unclear. And I can get close on the ease-out duration by calculating the duration as if it's linear and bumping it 20% or 30% or so.
Side note: there is some minor complexity here, removed to simplify the question, around the toValue of finishAnimation because of how the layer reports its current rotation value. If it's more than pi radians (180 degrees), it reports as negative. So it goes like this:
CGFloat rotationRadians = currRotation / M_PI;
if (rotationRadians < 0)
{
finishAnimation.duration = (-1 * rotationRadians); //negative means more than halfway through, or more than pi radians.
finishAnimation.toValue = #(0);
}
else
{
finishAnimation.duration = (2 - rotationRadians); //positive means less than halfway through, or less than pi radians
finishAnimation.toValue = #(M_PI * 2);
}
I am using a CABasicAnimation to rotate a UIImageView 90 degrees clockwise, but I need to have it rotate a further 90 degrees later on from its position after the initial 90 degree rotation.
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.duration = 10;
animation.additive = YES;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.fromValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(0)];
animation.toValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(90)];
[_myview.layer addAnimation:animation forKey:#"90rotation"];
Using the code above works initially, the image stays at a 90 degree angle. If I call this again to make it rotate a further 90 degrees the animation starts by jumping back to 0 and rotating 90 degrees, not 90 to 180 degrees.
I was under the impression that animation.additive = YES; would cause further animations to use the current state as a starting point.
Any ideas?
tl;dr: It is very easy to misuse removeOnCompletion = NO and most people don't realize the consequences of doing so. The proper solution is to change the model value "for real".
First of all: I'm not trying to judge or be mean to you. I see the same misunderstanding over and over and I can see why it happens. By explaining why things happen I hope that everyone who experience the same issues and sees this answer learn more about what their code is doing.
What went wrong
I was under the impression that animation.additive = YES; would cause further animations to use the current state as a starting point.
That is very true and it's exactly what happens. Computers are funny in that sense. They always to exactly what you tell them and not what you want them to do.
removeOnCompletion = NO can be a bitch
In your case the villain is this line of code:
animation.removedOnCompletion = NO;
It is often misused to keep the final value of the animation after the animation completes. The only problem is that it happens by not removing the animation from the view. Animations in Core Animation doesn't alter the underlying property that they are animating, they just animate it on screen. If you look at the actual value during the animation you will never see it change. Instead the animation works on what is called the presentation layer.
Normally when the animation completes it is removed from the layer and the presentation layer goes away and the model layer appears on screen again. However, when you keep the animation attached to the layer everything looks as it should on screen but you have introduced a difference between what the property says is the transform and how the layer appears to be rotated on screen.
When you configure the animation to be additive that means that the from and to values are added to the existing value, just as you said. The problem is that the value of that property is 0. You never change it, you just animate it. The next time you try and add that animation to the same layer the value still won't be changed but the animation is doing exactly what it was configured to do: "animate additively from the current value of the model".
The solution
Skip that line of code. The result is however that the rotation doesn't stick. The better way to make it stick is to change the model. Set the new end value of the rotation before animating the rotation so that the model looks as it should when the animation gets removed.
byValue is like magic
There is a very handy property (that I'm going to use) on CABasicAnimation that is called byValue that can be used to make relative animations. It can be combined with either toValue and fromValue to do many different kinds of animations. The different combinations are all specified in its documentation (under the section). The combination I'm going to use is:
byValue and toValue are non-nil. Interpolates between (toValue - byValue) and toValue.
Some actual code
With an explicit toValue of 0 the animation happens from "currentValue-byValue" to "current value". By changing the model first current value is the end value.
NSString *zRotationKeyPath = #"transform.rotation.z"; // The killer of typos
// Change the model to the new "end value" (key path can work like this but properties don't)
CGFloat currentAngle = [[_myview.layer valueForKeyPath:zRotationKeyPath] floatValue];
CGFloat angleToAdd = M_PI_2; // 90 deg = pi/2
[_myview.layer setValue:#(currentAngle+angleToAdd) forKeyPath:zRotationKeyPath];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:zRotationKeyPath];
animation.duration = 10;
// #( ) is fancy NSNumber literal syntax ...
animation.toValue = #(0.0); // model value was already changed. End at that value
animation.byValue = #(angleToAdd); // start from - this value (it's toValue - byValue (see above))
// Add the animation. Once it completed it will be removed and you will see the value
// of the model layer which happens to be the same value as the animation stopped at.
[_myview.layer addAnimation:animation forKey:#"90rotation"];
Small disclaimer:
I didn't run this code but am fairly certain that it runs as it should and that I didn't do any typos. Correct me if I did. The entire discussion is still valid.
pass incremental value of angle see my code
static int imgAngle=0;
- (void)doAnimation
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.duration = 5;
animation.additive = YES;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.fromValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(imgAngle)];
animation.toValue = [NSNumber numberWithFloat:DEGREES_TO_RADIANS(imgAngle+90)];
[self.imgView.layer addAnimation:animation forKey:#"90rotation"];
imgAngle+=90;
if (imgAngle>360) {
imgAngle = 0;
}
}
Above code is just for idea. Its not tested
If I want to animate UITableViewCell so it would bounce from left to right a few times, How can I do that? I'm trying that:
var bounds = activeCell.Bounds;
var originalLocation = bounds.Location;
var loc = originalLocation;
UIView.Animate(0.2,()=>{
loc.X = originalLocation.X + 20;
activeCell.Bounds = new RectangleF (loc, bounds.Size);
loc.X = originalLocation.X - 20;
activeCell.Bounds = new RectangleF (loc, bounds.Size);
});
It animates only the last state (i.e. moves element to the left). I tried to put them in separated Animate blocks - it didn't help. Tried to use different UIAnimationOptions - the same.
Here is a nice article explaining how to make it bounce.
http://khanlou.com/2012/01/cakeyframeanimation-make-it-bounce/
Moreover, there is an explanation the formula used to compute the bounce path.
For my personal use, I've taken the absolute value of the computation to simulate a rebound on ground.
- (void) displayNoCommentWithAnimation{
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"position.y"];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
animation.duration = 2;
int steps = 120;
NSMutableArray *values = [NSMutableArray arrayWithCapacity:steps];
double value = 0;
float e = 2.71;
for (int t = 0; t < steps; t++) {
value = 210 - abs(105 * pow(e, -0.025*t) * cos(0.12*t));
[values addObject:[NSNumber numberWithFloat:value]];
}
animation.values = values;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.delegate = self;
[viewThatNeedToBounce.layer addAnimation:animation forKey:nil];
}
- (void) animationDidStop:(CAAnimation *)animation finished:(BOOL)flag {
CAKeyframeAnimation *keyframeAnimation = (CAKeyframeAnimation*)animation;
[viewThatNeedToBounce.layer setValue:[NSNumber numberWithInt:210] forKeyPath:keyframeAnimation.keyPath];
[viewThatNeedToBounce.layer removeAllAnimations];
}
The problem with your approach is that UIView.Animate will record the changes that you make to your view, but only the final state for them.
If you change the Bounds property one hundred times in your animate block, only the last one is the one that will matter from the perspective of the animation framework.
CoreAnimation has a couple of quirks that are explained in the WWDC 2010 and WWDC2011 videos. They have great material and they explain a few of the tricks that are not very obvious.
That being said, animating cells in a UITableView is a complicated matter because you are really poking at a UITableView internals, so expect various strange side effects. You could lift the code from TweetStation that does that animation and deals with various corner cases. But even TweetStation and the Twitter for iOS app do not manage to be perfect, because you are animating things behind the back of a UIView that is constantly updating and making changes to very same properties you are animating.
From the top of my head, the easiest approach would be to put the animation code into a method and call that recursive as often as you want. Code untested, but it should work or at least give you an idea.
// Repeat 10 times, move 20 right and the left and right etc.
FancyAnim(activeCell, activeCell.Bounds.Location, 10, 20);
private void FancyAnim(UITableViewCell activeCell, PointF originalLocation, int repeat, float offset)
{
var bounds = activeCell.Bounds;
var loc = originalLocation;
UIView.Animate(0.2,
delegate
{
// Called when animation starts.
loc.X = originalLocation.X + offset;
activeCell.Bounds = new RectangleF (loc, bounds.Size);
},
delegate
{
// Called when animation ends.
repeat--;
// Call the animation method again but invert the movement.
// If you don't do this too often, you should not run out of memory because of a stack overflow.
if(repeat >= 0)
{
FancyAnim(activeCell, originalLocation, repeat, -offset);
}
});
You can however also use a path animation. You would define a path "20 units right, back to center, 20 units left, back to center" and repeat that animation as often as you like.
This requires you to deal with CAKeyFrameAnimation and will be slightly more code.
This site can get you jump started: http://www.bdunagan.com/2009/04/26/core-animation-on-the-iphone/
Lack of documentation and good samples sometimes really makes even simple tasks so annoyingly challenging.
Here is the solution
Sure code isn't elegant, but it works. Hope it will someday help somebody else, so he or she wouldn't need to spend half a day on something stupidly simple like that
var activeCell = ((Element)sender).GetActiveCell();
var animation =
(CAKeyFrameAnimation)CAKeyFrameAnimation.FromKeyPath ("transform.translation.x");
animation.Duration = 0.3;
animation.TimingFunction = // small details matter :)
CAMediaTimingFunction.FromName(CAMediaTimingFunction.EaseOut.ToString());
animation.Values = new NSObject[]{
NSObject.FromObject (20),
NSObject.FromObject (-20),
NSObject.FromObject (10),
NSObject.FromObject (-10),
NSObject.FromObject (15),
NSObject.FromObject (-15),
};
activeCell.Layer.AddAnimation (animation,"bounce");
I am using the following CABasicAnimation. But, its very slow..is there a way to speed it up ? Thanks.
- (void)spinLayer:(CALayer *)inLayer duration:(CFTimeInterval)inDuration
direction:(int)direction
{
CABasicAnimation* rotationAnimation;
// Rotate about the z axis
rotationAnimation =
[CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
// Rotate 360 degress, in direction specified
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2.0 * direction];
// Perform the rotation over this many seconds
rotationAnimation.duration = inDuration;
// Set the pacing of the animation
rotationAnimation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// Add animation to the layer and make it so
[inLayer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
}
Core Animation animations can repeat a number of times, by setting the repeatCount property on the animation.
So if you'd like to have an animation run for a total 80 seconds, you need to figure out a duration for one pass of the animation – maybe one full spin of this layer – and then set the duration to be that value. Then let the animation repeat that full spin several times to fill out your duration.
So something like this:
rotationAnimation.repeatCount = 8.0;
Alternatively, you can use repeatDuration to achieve a similar affect:
rotationAnimation.repeatDuration = 80.0;
In either case, you need to set the duration to the time of a single spin, and then repeat it using ONE of these methods. If you set both properties, the behavior is undefined. You can check out the documentation on CAMediaTiming here.