While i'm implementing a game, i'm just front of a matter, whose really easy i think, but don't know how to fix it. I'm a bit new in objective-c as you could see with my reputation :(
The problem is, i have an animation, which works correctly. Here is the code :
CABasicAnimation * bordgauche = [CABasicAnimation animationWithKeyPath:#"transform.translation.x"];
bordgauche.fromValue = [NSNumber numberWithFloat:0.0f];
bordgauche.toValue = [NSNumber numberWithFloat:749.0f];
bordgauche.duration = t;
bordgauche.repeatCount = 1;
[ImageSuivante.layer addAnimation:bordgauche forKey:#"bordgauche"];
And i want to get the current position of my image. So i use :
CALayer *currentLayer = (CALayer *)[ImageSuivante.layer presentationLayer];
currentX = [(NSNumber *)[currentLayer valueForKeyPath:#"transform.translation.x"] floatValue];
But i don't get it instantly. I get one time "Current = 0.0000", which is the starting value, when i use a nslog to print it, but not the others after.
I don't know how to get the instant position of my image, currentX, all the time.
I expect i was understable.
Thanks for your help :)
You can get the value from your layer's presentation layer.
CGPoint currentPos = [ImageSuivante.layer.presentationLayer position];
NSLog(#"%f %f",currentPos.x,currentPos.y);
I think you have 3 options here (pls comment if more exist):
option1: split your first animation into two and when the first half ends start the second half of the animation plus the other animation
...
bordgauche.toValue = [NSNumber numberWithFloat:749.0f / 2];
bordgauche.duration = t/2;
bordgauche.delegate = self // necessary to catch end of anim
[bordgauche setValue:#"bordgauche_1" forKey: #"animname"]; // to identify anim if more exist
...
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
if ([theAnimation valueForKey: #"animname"]==#"bordgauche_1") {
CABasicAnimation * bordgauche = [CABasicAnimation
animationWithKeyPath:#"transform.translation.x"];
bordgauche.fromValue = [NSNumber numberWithFloat:749.0f / 2];
bordgauche.toValue = [NSNumber numberWithFloat:749.0f];
bordgauche.duration = t/2;
bordgauche.repeatCount = 1;
[ImageSuivante.layer addAnimation:bordgauche forKey:#"bordgauche_2"];
// plus start your second anim
}
option2: setup a NSTimer or a CADisplayLink (this is better) callback and check continuously the parameters of your animating layer. Test the parameters for the required value to trigger the second anim.
displayLink = [NSClassFromString(#"CADisplayLink") displayLinkWithTarget:self selector:#selector(check_ca_anim)];
[displayLink setFrameInterval:1];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
... or
animationTimer = [NSTimer scheduledTimerWithTimeInterval:(NSTimeInterval)(1.0 / 60.0) target:self selector:#selector(check_ca_anim) userInfo:nil repeats:TRUE];
[[NSRunLoop mainRunLoop] addTimer:animationTimer forMode:NSRunLoopCommonModes];
- (void) check_ca_anim {
...
CGPoint currentPosition = [[ImageSuivante.layer presentationLayer] position];
// test it and conditionally start something
...
}
option3: setup a CADisplayLink (can be called now as "gameloop") and manage the animation yourself by calculating and setting the proper parameters of the animating object. Not knowing what kind of game you would like to create I would say game loop might be useful for other game specific reasons. Also here I mention Cocos2d which is a great framework for game development.
You can try getting the value from your layer's presentation layer, which should be close to what is being presented on screen:
[ [ ImageSuivante.layer presentationLayer ] valueForKeyPath:#"transform.translation.x" ] ;
I would add one option to what #codedad pointed out. What's interesting, it gives a possibility of getting instant values of your animating layer precisely (you can see CALayer's list of animatable properties) and it's very simple.
If you override method
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
in your UIView class (yep, you'd need to implement a custom class derived from UIView for your "ImageSuivante" view), then the parameter layer which is passed there would give you exactly what you need. It contains precise values used for drawing.
E.g. in this method you could update a proper instant variable (to work with later) and then call super if you don't do drawing with you hands:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context {
self.instantOpacity = layer.opacity;
self.instantTransform = layer.transform;
[super drawLayer:layer inContext:context];
}
Note that layer here is generally not the same object as self.layer (where self is your custom UIView), so e.g. self.layer.opacity will give you the target opacity, but not the instant one, whereas self.instantOpacity after the code above will contain the instant value you want.
Just be aware that this is a drawing method and it may be called very frequently, so no unnecessary calculations or any heavy operations there.
The source of this idea is Apple's Custom Animatable Property project.
Related
I need a complex continuous animation of a UIView that involves setting CATransform3D rotation and translation properties that need to be calculated, so a standard animation is no option.
I turned to using CALayer animation. And have this:
self.displayLink = [self.window.screen displayLinkWithTarget:self selector:#selector(update:)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
- (void)update:(CADisplayLink*)sender
{
CGFloat elapsedTime = sender.timestamp - self.lastTimestamp;
self.lastTimestamp = sender.timestamp;
self.rotation += elapsedTime * 0.1; // Factor determines speed.
// This is just example for SO post; the real animation is more complicated!
CATransform3D transform;
transform = CATransform3DMakeRotation(self.rotation, 1.0, 0.0, 0.0);
self.imageLayer.transform = transform;
}
Here self.imageLayer is a CALayer whose contents has been set with an image and added as a sublayer to my base-view.
It does rotate 'a bit' but not continuously, sometimes it seems to stop or rotate backwards a bit.
It seems that assigning a new transform quite often does not have any effect because the self.rotation value is incremented much much more. Adding a [self setNeedsDisplay] did not help.
I've not done much with CALayers yet so I guess that I'm missing something very basic. Tried to find it for some time but all examples I found seem too far from what I want.
When you create your own layer and then modify its properties, it animates them implicitly (see “Animating Simple Changes to a Layer’s Properties”. You're not seeing the changes you expect because Core Animation is animating the changes rather than applying them instantly.
You need to disable implicit animation. There are several ways to disable it; see “Changing a Layer’s Default Behavior”. Or search stack overflow for “disable implicit animations”. Here's one way:
NSDictionary *actions = #{ #"transform": [NSNull null] };
self.imageLayer.actions = actions;
Just do that once, where you create imageLayer.
So, I am fairly new to iOS programming, and have inherited a project from a former coworker. We are building an app that contains a gauge UI. When data comes in, we want to smoothly rotate our "layer" (which is a needle image) from the current angle to a new target angle. Here is what we have, which worked well with slow data:
-(void) MoveNeedleToAngle:(float) target
{
static float old_Value = 0.0;
CABasicAnimation *rotateCurrentPressureTick = [CABasicAnimation animationWithKeyPath:#"transform.rotation");
[rotateCurrentPressureTick setDelegate:self];
rotateCurrentPressureTick.fromValue = [NSSNumber numberWithFloat:old_value/57.2958];
rotateCurrentPressureTick.removedOnCompletion=NO;
rotateCurrentPressureTick.fillMode=kCAFillModeForwards;
rotateCurrentPressureTick.toValue=[NSSNumber numberWithFloat:target/57.2958];
rotateCurrentPressureTick.duration=3; // constant 3 second sweep
[imageView_Needle.layer addAnimation:rotateCurrentPressureTick forKey:#"rotateTick"];
old_Value = target;
}
The problem is we have a new data scheme in which new data can come in (and the above method called) faster, before the animation is complete. What's happening I think is that the animation is restarted from the old target to the new target, which makes it very jumpy.
So I was wondering how to modify the above function to add a continuous/restartable behavior, as follows:
Check if the current animation is in progress and
If so, figure out where the current animation angle is, and then
Cancel the current and start a new animation from the current rotation to the new target rotation
Is it possible to build that behavior into the above function?
Thanks. Sorry if the question seems uninformed, I have studied and understand the above objects/methods, but am not an expert.
Yes you can do this using your existing method, if you add this bit of magic:
- (void)removeAnimationsFromView:(UIView*)view {
CALayer *layer = view.layer.presentationLayer;
CGAffineTransform transform = layer.affineTransform;
[layer removeAllAnimations];
view.transform = transform;
}
The presentation layer encapsulates the actual state of the animation. The view itself doesn't carry the animation state properties, basically when you set an animation end state, the view acquires that state as soon as you trigger the animation. It is the presentation layer that you 'see' during the animation.
This method captures the state of the presentation layer at the exact moment you cancel the animation, and then applies that state to the view.
Now you can use this method in your animation method, which will look something like this:
-(void) MoveNeedleToAngle:(float) target{
[self removeAnimationsFromView:imageView_Needle];
id rotation = [imageView_Needle valueForKeyPath:#"layer.transform.rotation.z"];
CGFloat old_value = [rotation floatValue]*57.2958;
// static float old_value = 0.0;
CABasicAnimation *rotateCurrentPressureTick = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
[rotateCurrentPressureTick setDelegate:self];
rotateCurrentPressureTick.fromValue = [NSNumber numberWithFloat:old_value/57.2958];
rotateCurrentPressureTick.removedOnCompletion=NO;
rotateCurrentPressureTick.fillMode=kCAFillModeForwards;
rotateCurrentPressureTick.toValue=[NSNumber numberWithFloat:target/57.2958];
rotateCurrentPressureTick.duration=3; // constant 3 second sweep
[imageView_Needle.layer addAnimation:rotateCurrentPressureTick forKey:#"rotateTick"];
old_value = target;
}
(I have made minimal changes to your method: there are a few coding style changes i would also make, but they are not relevant to your problem)
By the way, I suggest you feed your method in radians, not degrees, that will mean you can remove those 57.2958 constants.
You can get the current rotation from presentation layer and just set the toValue angle. No need to keep old_value
-(void) MoveNeedleToAngle:(float) targetRadians{
CABasicAnimation *animation =[CABasicAnimation animationWithKeyPath:#"transform.rotation"];
animation.duration=5.0;
animation.fillMode=kCAFillModeForwards;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
animation.removedOnCompletion=NO;
animation.fromValue = [NSNumber numberWithFloat: [[layer.presentationLayer valueForKeyPath: #"transform.rotation"] floatValue]];
animation.toValue = [NSNumber numberWithFloat:targetRadians];
// layer.transform= CATransform3DMakeRotation(rads, 0, 0, 1);
[layer addAnimation:animation forKey:#"rotate"];
}
Another way i found (commented line above) is instead of using fromValue and toValue just set the layer transform. This will produce the same animation but the presentationLayer and the model will be in sync.
I've made a wrapper for compact creation of CABasicAnimation instances.
It's implemented through a category for UIView as an instance method named change:from:to:in:ease:delay:done:. So for example, I can do:
[self.logo
change:#"y"
from:nil // Use current self.logo.layer.position.y
to:#80
in:1 // Finish in 1000 ms
ease:#"easeOutQuad" // A selector for a CAMediaTimingFunction category method
delay:0
done:nil];
The problem
When the CABasicAnimation starts, animationDidStart: handles setting self.logo.layer.position.y to 80 (the end value). Before it worked like this, I tried using animationDidStop:finished: to do the same thing, but found the layer flickering after completing the animation. Now, the layer goes straight to the end value, and no interpolation occurs. I implemented animationDidStart: in my UIView category like so:
- (void)animationDidStart:(CAAnimation *)animation
{
[self.layer
setValue:[animation valueForKey:#"toValue"]
forKeyPath:[animation valueForKey:#"keyPath"]];
}
I'm setting the end value in order to match the model layer with the presentation layer (in other words, to prevent resetting back to the start position).
Here's the implementation to change:from:to:in:ease:delay:done:...
- (CABasicAnimation*) change:(NSString*)propertyPath
from:(id)from
to:(id)to
in:(CGFloat)seconds
ease:(NSString*)easeName
delay:(CGFloat)delay
done:(OnDoneCallback)done
{
NSString* keyPath = [app.CALayerAnimationKeyPaths objectForKey:propertyPath];
if (keyPath == nil) keyPath = propertyPath;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:keyPath];
if (delay > 0) animation.beginTime = CACurrentMediaTime() + delay;
if (easeName != nil) animation.timingFunction = [CAMediaTimingFunction performSelector:NSSelectorFromString(easeName)];
if (from != nil) animation.fromValue = from;
animation.toValue = to;
animation.duration = seconds;
animation.delegate = self;
[self.layer setValue:done forKey:#"onDone"];
[self.layer addAnimation:animation forKey:keyPath];
return animation;
}
In the first line of the above code, this is the NSDictionary I use to convert property shortcuts to the real keyPath. This is so I can just type #"y" instead of #"position.y" every time.
app.CALayerAnimationKeyPaths = #{
#"scale": #"transform.scale",
#"y": #"position.y",
#"x": #"position.x",
#"width": #"frame.size.width",
#"height": #"frame.size.height",
#"alpha": #"opacity",
#"rotate": #"transform.rotation"
};
Any questions?
What you're seeing is, I think, expected behavior. You are setting an animatable property after animation of that property has started. That is in fact the most common and recommended way to do exactly what you are seeing happen, i.e. cancel the animation and jump right to the final position now. So you are deliberately canceling your own animation the minute it gets going.
If that's not what you want, don't do that. Just set the animated property to its final value before the animation is attached to the layer - being careful, of course, not to trigger implicit animation as you do so.
I'm trying to make a sequence of animations, I've found in CAAnimationGroup the right class to achieve that object. In practice I'm adding on a view different subviews and I'd like to animate their entry with a bounce effect, the fact is that I want to see their animations happening right after the previous has finished. I know that I can set the delegate, but I thought that the CAAnimationGroup was the right choice.
Later I discovered that the group animation can belong only to one layer, but I need it on different layers on screen. Of course on the hosting layer doesn't work.
Some suggestions?
- (void) didMoveToSuperview {
[super didMoveToSuperview];
float startTime = 0;
NSMutableArray * animArray = #[].mutableCopy;
for (int i = 1; i<=_score; i++) {
NSData *archivedData = [NSKeyedArchiver archivedDataWithRootObject: self.greenLeaf];
UIImageView * greenLeafImageView = [NSKeyedUnarchiver unarchiveObjectWithData: archivedData];
greenLeafImageView.image = [UIImage imageNamed:#"greenLeaf"];
CGPoint leafCenter = calculatePointCoordinateWithRadiusAndRotation(63, -(M_PI/11 * i) - M_PI_2);
greenLeafImageView.center = CGPointApplyAffineTransform(leafCenter, CGAffineTransformMakeTranslation(self.bounds.size.width/2, self.bounds.size.height));
[self addSubview:greenLeafImageView];
//Animation creation
CAKeyframeAnimation *bounceAnimation = [CAKeyframeAnimation animationWithKeyPath:#"transform.scale"];
greenLeafImageView.layer.transform = CATransform3DIdentity;
bounceAnimation.values = #[
[NSNumber numberWithFloat:0.5],
[NSNumber numberWithFloat:1.1],
[NSNumber numberWithFloat:0.8],
[NSNumber numberWithFloat:1.0]
];
bounceAnimation.duration = 2;
bounceAnimation.beginTime = startTime;
startTime += bounceAnimation.duration;
[animArray addObject:bounceAnimation];
//[greenLeafImageView.layer addAnimation:bounceAnimation forKey:nil];
}
// Rotation animation
[UIView animateWithDuration:1 animations:^{
self.arrow.transform = CGAffineTransformMakeRotation(M_PI/11 * _score);
}];
CAAnimationGroup * group = [CAAnimationGroup animation];
group.animations = animArray;
group.duration = [[ animArray valueForKeyPath:#"#sum.duration"] floatValue];
[self.layer addAnimation:group forKey:nil];
}
CAAnimationGroup is meant for having multiple CAAnimation subclasses being stacked together to form an animation, for instance, one animation can perform an scale, the other moves it around, while a third one can rotate it, it's not meant for managing multiple layers, but for having multiple overlaying animations.
That said, I think the easiest way to solve your issue, is to assign each CAAnimation a beginTime equivalent to the sum of the durations of all the previous ones, to illustrate:
for i in 0 ..< 20
{
let view : UIView = // Obtain/create the view...;
let bounce = CAKeyframeAnimation(keyPath: "transform.scale")
bounce.duration = 0.5;
bounce.beginTime = CACurrentMediaTime() + bounce.duration * CFTimeInterval(i);
// ...
view.layer.addAnimation(bounce, forKey:"anim.bounce")
}
Notice that everyone gets duration * i, and the CACurrentMediaTime() is a necessity when using the beginTime property (it's basically a high-precision timestamp for "now", used in animations). The whole line could be interpreted as now + duration * i.
Must be noted, that if a CAAnimations is added to a CAAnimationGroup, then its beginTime becomes relative to the group's begin time, so a value of 5.0 on an animation, would be 5.0 seconds after the whole group starts. In this case, you don't use the CACurrentMediaTime()
If you review the documentation, you will note that CAAnimationGroup inherits from CAAnimation, and that CAAnimation can only be assigned to one CALayer. It's intent is really to make it easy to create and manage multiple animations you wish to apply to a CALayer at the same time, not to manager animations for multiple CALayer objects.
To handle the sequencing of different animations between different CALayer or UIViewobjects, a technique I use is to create an NSOperation for each object/animation, then throw them into a NSOperationQueue to manage the sequencing. This is a bit complicated as you have to use the animation completion callback to tell the NSOperation it is finished, but if you write a good animation management subclass of NSOperation, it can be rather convenient and allow you to create sophisticated sequencing paths. The low-rent way of accomplishing the sequencing goal is to simply set the beginTime property on your CAAnimation object (which comes from it's adoption of the CAMediaTiming protocol) as appropriate to get the timing you want.
With that said, I am going to point you to some code that I wrote and open-sourced to solve the exact same use case you describe. You may find it on github here (same code included). I will add the following notes:
My animation management code allow your to define your animation in a plist by identifying the sequence and timing of image changes, scale changes, position changes, etc. It's actually pretty convenient and cleaner to adjust your animation in a plist file rather than in code (which is why I wrote this).
If the user is not expected to interact with the subviews you creating, it's actually much better (less overhead) to create layer objects that are added as sub-layers to your hosting view's layer.
I have an UIImageView that runs across the screen when a button is pressed and held. When the button is pressed is changes the UIImage of the UIImageView and when the button is let go I change it to its original UIImage. When ever the image changes back it snaps back to the location that the image started.
This Timer is called when the button is pressed:
//This is the image that changes when the button is pressed.
imView.image = image2;
runTimer = [NSTimer scheduledTimerWithTimeInterval:0.04
target:self
selector:#selector(perform)
userInfo:nil
repeats:YES];
This is called When the button stops being held:
- (IBAction)stopPerform:(id)sender{
[runTimer invalidate];
//THIS IS WHAT SNAPS THE ANIMATION BACK:
//Without this the animation does not snap back
imView.image = image1;
}
- (void)performRight{
CGPoint point0 = imView.layer.position;
CGPoint point1 = { point0.x + 4, point0.y };
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"position.x"];
anim.fromValue = #(point0.x);
anim.toValue = #(point1.x);
anim.duration = 0.2f;
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
// First we update the model layer's property.
imView.layer.position = point1;
// Now we attach the animation.
[imView.layer addAnimation:anim forKey:#"position.x"];
}
Do I need to add the change in images to the animation? If so how? Im really confused.
Core Animation uses different sets of properties to represent an object:
From Core Animation Programming Guide:
model layer tree (or simply “layer tree”) are the ones your app interacts with the most. The objects in this tree are the model objects that store the target values for any animations. Whenever you change the property of a layer, you use one of these objects.
presentation tree contain the in-flight values for any running animations. Whereas the layer tree objects contain the target values for an animation, the objects in the presentation tree reflect the current values as they appear onscreen. You should never modify the objects in this tree. Instead, you use these objects to read current animation values, perhaps to create a new animation starting at those values.
So when you animate the properties you change the presentation layer, but once the animation is finished the object reverts back to its model property values.
What you need to do to fix this is use the [CAAnimation animationDidStop:finished:] delegate method to set the final property value and anything else you would like to do. I think you could use this to dump that horrible NSTimer code you are using and one small part of the world will be that much better.