I have the code below to animate a score label. How would you go about changing it so it becomes an ease out animation?
Thanks
- (void)animateFrom:(float)fromValue toValue:(float)toValue
{
self.scoreAnimationFrom = fromValue;
self.scoreAnimationTo = self.question.correctValue;
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:#selector(animateNumber:)];
self.startTimeInterval = CACurrentMediaTime();
[link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)animateNumber:(CADisplayLink *)link
{
float dt = ([link timestamp] - self.startTimeInterval) / self.duration;
if (dt >= 1.0)
{
[link removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
return;
}
float current = ((self.scoreAnimationTo - self.scoreAnimationFrom) * dt + self.scoreAnimationFrom);
self.valueLabel.text = [NSString stringWithFormat:#"%f", current];
}
The progress of your animation is described by the dt variable, which is a value between 0 and 1. Applying an ease out timing to your animation is as simple as funnelling this value into an appropriate timing function before applying it further. The responsibility of a timing function is to turn the original value into another value between 0 and 1, following a specific timing curve. For more information about timing functions, simply refer to Apple documentation.
In your case, you therefore need to apply an ease out timing function to dt, something like:
dt = [[TimingFunction easeOutTimingFunction] solveForInput:dt];
Core Animation provides the CAMediaTimingFunction class, but sadly its _solveForInput: solving method is private. There exists several open-source implementations of timing functions that you can use instead, e.g. https://github.com/warrenm/AHEasing.
If you are curious, I also recently implemented a method equivalent to _solveForInput: within a category of CAMediaTimingFunction.
I have made a very simple solution by using a sin function instead of the complex curve calculation with polynomial coefficients:
//EasyOut
dt = sin(dt*M_PI/2)+0.01f;
Enjoy it :)
Related
Problem:
If you take a look at my current code, you'll see that it works fine if targetSeconds is higher than ~2-3 seconds.
However, it will not work if targetSeconds is 0.005 seconds because there's no way it can finish 100 method calls in 0.005 seconds. Therefore, does anyone have any suggestions to what I can do to improve it? I'd rather not include third party GitHub repositories.
Current code:
// Target seconds means the seconds that it'll take to become 100.0f.
- (void)startAnimatingWithTargetSeconds:(NSTimeInterval)targetSeconds{
// Try to set timeInterval to 0.005f and you'll see that it won't finish in 0.005f
[NSTimer scheduledTimerWithTimeInterval:targetSeconds / 100.0f target:self selector:#selector(animateWithTimer:) userInfo:nil repeats:YES];
}
- (void)animateWithTimer:(NSTimer *)timer {
BOOL isFinished = self.currentProgress >= 100;
if (isFinished) {
// Invalidate timer
if (timer.isValid) {
[timer invalidate];
}
// Reset currentProgress
self.currentProgress = 0.0f;
}else{
if (timer.isValid) {
self.currentProgress += 1;
}
}
}
// Overriden setter
- (void)setCurrentProgress:(CGFloat)currentProgress {
if (_currentProgress == currentProgress) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
_currentProgress = currentProgress;
[self setNeedsDisplay];
});
}
And then in drawRect, I have an UIBezierPath that basically draws the circle depending on self.currentProgress.
Something like this: CGFloat endAngle = (self.currentProgress / 100.0f) * 2 * M_PI + startAngle;
Question:
Is there any formula or anything that'll help me in my case? Because if I were to set self.currentProgress to self.currentProgress += 5; instead of 1, it'll animate a lot faster, which is precisely what I'm looking for.
First of all, when would you want to redraw every 0.005 seconds? That's 200 FPS, way more than you need.
Don't reinvent the wheel – leverage Core Animation! Core Animation already knows how to call your state change function at the proper rate, and how to redraw views as necessary, assuming you tell it what to do. The gist of this strategy is as follows:
Add a dynamic property to your layer that represents the completeness of your pie slice.
Tell Core Animation that this property can be animated by either overriding actionForKey:, or setting the animation into the actions dictionary (or even more options, detailed here).
Tell Core Animation that changes to this property require redraws of the layer using needsDisplayForKey:.
Implement the display method to redraw the pie based on the presentation layer's value of your dynamic property.
Done! Now you can animate the dynamic property from any value to any other, just as you would opacity, position, etc. Core Animation takes care of the timing and the callbacks, and you get a buttery smooth 60 FPS.
For some examples, see the following resources, listed in order of decreasing usefulness (in my opinion):
Animating Pie Slices using a custom CALayer – this is basically what you want to do
Animating Custom Layer Properties – better written but a bit less applicable
Apple's Core Animation Guide – esoteric, but worth a read if you want to master the strange beast that is Core Animation
Good luck!
I prefer use something like this, because timer (and usleep) on small intervals works very inaccurately:
-(void)startAnimatingWithTargetSeconds:(NSTimeInterval)targetSeconds
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
float fps = 60;
float currentState = 0;
float frameStateChange = fps/targetSeconds;
NSDate *nextFrameDate = [NSDate date];
while (currentState < 1) {
currentState += frameStateChange;
self.currentProgress = roundf(currentState * 100.);
nextFrameDate = [nextFrameDate dateByAddingTimeInterval:1./fps];
while ([nextFrameDate timeIntervalSinceNow] > 0) {
usleep((useconds_t)(100000/fps));
}
}
self.currentProgress = 0;
});
}
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.
Ok so recently I made a project which had like a bit of a gravity timer,
int speed = 5;
NSTimer *gravitytimer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:#selector(gravity) userInfo:nil repeats:YES];
-(void)gravity {
image.center = CGPointMake(image.center.x, image.center.y - speed)
}
The problem is that the speed keeps multiplying or keeps adding and the image goes faster and faster. I don't know what the problem is. I am a newbie so sorry if this is a bad question. Thanks to all the people who take the time to answer.
When you create a timer, while it will try to call it every 0.01 seconds, you actually have no assurances that it will be called precisely at that rate (i.e. if it takes too long, it may skip one or two; if the main thread is blocked doing something else (and it's quite busy when the app first starts), you may skip quite a few). You can request to update image.center 100 times per second, but you have no guarantee that this is how often it actually will end up getting called.
If you wanted to use this technique to animate, you do so in a manner that isolates the speed of the animation from the frequency of the timer's selector is actually called. So, you might capture the start time, e.g.
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
CGPoint startPoint = image.center;
And then in your timer routine, you'd capture the elapsed time, multiply your speed times the elapsed time, and go from there:
-(void)gravity {
CFAbsoluteTime elapsed = CFAbsoluteTimeGetCurrent() - self.start;
CGFloat speed = 100.0; // e.g. 100 points per second
CGPoint center = startPoint;
center.y += (elapsed * speed);
image.center = center;
}
As an aside, I assume this is just an exercise in timers. If this were a real animation, there are far more effective ways to do such animation (e.g. block-based animateWithDuration, UIKit Dynamics, and if you really wanted to do something timer-based, you'd often use CADisplayLink rather than a NSTimer). Also, BTW, the frame rate of the display is 60fps, so there's no point in trying to call it more frequently than that.
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.
I have one UIImageView and number of UIImageView which are entering in screen after some time interval. I want to check if that one ImageView is collided with any others.
Please help me.
The general process for detecting collisions between rectangular shaped views is to use CGRectIntersectsRect() to see if the frames of two views intersect. So, if you have a NSMutableArray of UIImageView objects, you can perform a fast enumeration through them and look for the collision, something like:
for (UIView* view in self.imageViewsArray)
{
if (CGRectIntersectsRect(view.frame, viewToDetectCollisionWith.frame))
{
// do whatever you want when you detect the collision
}
}
Or, you can use the enumerateObjectsUsingBlock which uses fast enumeration, but gives you both the numeric index, idx, and the individual UIView objects in the array in a single statement:
[self.imageViewsArray enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL *stop) {
if (CGRectIntersectsRect(view.frame, viewToDetectCollisionWith.frame))
{
// do whatever you want when you detect the collision
}
}];
Original answer:
If you're animating the UIImageView objects via the various automated animation techniques, you have to use something like CADisplayLink to check for collisions because iOS is taking care of the animation and you otherwise are not informed of the frame of the various views in the middle of an animation. The CADisplayLink informs your app every time the animation has progressed, so you get information about the location of views as the animation progresses. Sounds like you're not availing yourself of built in animation techniques, but rather using a NSTimer to manually adjust frames, so you might not need the below code. But if you ever pursue a more automated animation, you can use the following technique.
What you can do is use a CADisplayLink to get information about the screen while the animation is in progress:
#import <QuartzCore/QuartzCore.h>
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(displayLinkHandler)];
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
You might even want to store that in a class property so you can add it and remove it as the view appears and disappears:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(displayLinkHandler)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.displayLink removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
self.displayLink = nil;
}
Then, you can start your animation. I'm just using the standard block-based animation to continually animate the changing of two image view frames, but you'll obviously do whatever is appropriate for your app:
[UIView animateWithDuration:4.0
delay:0.5
options:UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat
animations:^{
self.imageView1.frame = ... // I set the frame here
self.imageView2.frame = ... // I set the frame here
}
completion:nil];
Now I can detect when these two frames collide (i.e., whether their frames intersect) with this CADisplayLink handler which grabs the relevant presentationLayer properties to get the "in progress" frame coordinates:
- (void)displayLinkHandler
{
id presentationLayer1 = self.imageView1.layer.presentationLayer;
id presentationLayer2 = self.imageView2.layer.presentationLayer;
BOOL nowIntersecting = CGRectIntersectsRect([presentationLayer1 frame], [presentationLayer2 frame]);
// I'll keep track of whether the two views were intersected in a class property,
// and therefore only display a message if the intersected state changes.
if (nowIntersecting != self.wasIntersected)
{
if (nowIntersecting)
NSLog(#"imageviews now intersecting");
else
NSLog(#"imageviews no longer intersecting");
self.wasIntersected = nowIntersecting;
}
}
By the way, you may need to add Quartz 2D, the QuartzCore.framework, to your project. See the Project Editor Help.