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.
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 have some problem with nstimer and ui update and drawrect,
I have a viewcontroller and put a timer inside, it runs every 0.02 secs,
in this timer tick function, I make an imageview move from top to bottom( change centre of view),
then add another view on, and when touch begin touchmove and touchend on this view,
draw a line and call setneedsdisplay, when my fingure moves on the view, the imageview i mentioned before, moves slower,
by checking the time tick, I found that, without finger on, it ticks 0.02 secs, but when
move on, it slows to abt 0.1 sec, which make the imageview move slower,
any other way to optimise it,
I think the setneedsdisplay did the thick,
ofcourse, I try to change the runloop mode with
[[NSRunLoop currentRunLoop] addTimer:tickTimer forMode:NSDefaultRunLoopMode];
not help.
pls help, and another question is will another thread help this?
I tried nsthread, seems not help.... lol
For this purpose a much better solution is a CADisplayLink, that calls a method each time a frame is going to be refreshed. That way you'll avoid desynchronization between the timer and the actual framerate.
You set up a display link e.g. like this:
_dl = [CADisplayLink displayLinkWithTarget: self selector:#selector(refreshFrame)];
[_dl addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[_dl setPaused:NO];
and in the refreshFrame callback you can update the screen. You can make use of the duration and frameInterval to calculate the time that passed from the last refresh and adjust the movement speed accordingly:
-(void) refreshFrame
{
CGFloat speed = 10.0; //points per second
CGFloat timePassed = (_dl.duration * _dl.frameInterval)];
//refresh the view (example)
CGPoint imageCenter = self.imageView.center;
imageCenter y += speed * timePassed;
self.imageView.center = imageCenter;
}
This way the speed of the view stays constant even as the framerate changes in time.
It is wrong to assume the timer event is going to be called with exact intervals. Calculate the new parameters based on the speed and velocity vectors and time from the last update.
I have a number of images I run continuous animations on. How does one update these running animations smoothly so that there is no visible transition between them, unless part of the animation.
e.g. rotating and scaling an image, and then updating the animation to rotate as it was, but scale up slightly. I currently get a visible change.
I can imagine that that scaling should be done within an animation block and them just run the rotation animation as before, the issue would be then that the rotation would stop while it scales up.
I need it to be seamless. e.g. this code block does not cause a smooth scaling, even though it uses UIViewAnimationOptionBeginFromCurrentState
[UIView animateWithDuration:0.5
delay:0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut //| UIViewAnimationOptionRepeat
animations:(void (^)(void)) ^{
imageViewToScale.transform = CGAffineTransformMakeScale(1.2, 1.2);
}
completion:^(BOOL finished){
//imageViewToScale.transform=CGAffineTransformIdentity;
}];
To be honest if your going to have continuous animations on your views that will alter there states at run time UIView animations isn't he best approach and might cause jitter whilst the completion handler is called over and over e.t.c.
However is you want to do 2 animations in the block you can either set the scale and the rotation in the same block and they will happend simultaneously. Or you could call a function that starts the block and on completion calls it again to see if there are any new animations for that view. When there are none, the completion block just stops, sort of using them recursively, though this isn't the approach i wouldn't recommend if you are doing a lot of animations on a lot of views continuouly.
In OpenGLES you use NSTimer to run continuous animation that would handle the updating of all animations in your app. If you go this route its a lot more hard work and you need to implement the easing/quadratic curve functions for smooth animation yourself. However if you extend the clases you could set states for each image and then when the timer fires you could update its transformation based one the states you have given it. So when it has rotated a certain amount of degrees/radians then you could set it to scale. Now to keep animation smoothly you will need to use NSTimerIntervals to make sure you multiply the distance moved by time elapsed in order to get smooth animation. Personally this is the route i use for things that might be moving on screen constantly but might be over kill if you need to only move things twice and then be done.
EDIT: The code for doing the second step as you asked!
So you need to declare a NSTimer that wil poll your animation steps and an NSTimeInterval so that you update your animation each step only by the amount of time that has passed.
NSTimer *animationTimer;
NSTimeInterval animationInterval;
NSTimeInterval lastUpdateTime;
float currentRotation;
float current scale;
Thirdly you need to set up an NSTimer to fire off updating of your views:
- (void)startAnimation
{
animationTimer = [NSTimer scheduledTimerWithTimeInterval:animationInterval target:self selector:#selector(drawView:) userInfo:nil repeats:YES];
}
- (void)stopAnimation
{
[animationTimer invalidate];
animationTimer = nil;
}
Then you need to kick the thing off when your view starts or when you want to start anmation something like this works
- (void)setAnimationInterval:(NSTimeInterval)interval
{
animationInterval = interval;
if(animationTimer)
{
[self stopAnimation];
[self startAnimation];
}
}
Then In your drawView: Method you need to update your transforms based on time elapsed between each fire of the timer so that the animation is smooth and constant over time. Essentially a linear transform at this point.
- (void)drawView:(id)sender
{
if(lastUpdateTime == -1)
{
lastUpdateTime = [NSDate timeIntervalSinceReferenceDate];
}
NSTimeInterval timeSinceLastUpdate = [NSDate timeIntervalSinceReferenceDate] - lastUpdateTime;
currentRotation = someArbitrarySCALEValue * timeSinceLastUpdate;
currentScale = someArbitraryROTATIONValue * timeSinceLastUpdate;
CGAffineTransform scaleTrans = CGAffineTransformMakeScale(currentScale,currentScale);
CGAffineTransform rotateTrans = CGAffineTransformMakeRotation(currentRotation * M_PI / 180);
imageViewToScale.transform = CGAffineTransformConcat(scaleTrans, rotateTrans);
lastUpdateTime = [NSDate timeIntervalSinceReferenceDate];
}
Note this will not add easing to your animations nor will it at physics to the stopping you will need to play around with that. Also you may need to make sure that when the rotation goes over 360 you reset it to 0 and that you convert between degrees and radians respectively.
Look into using physics for things like bounces, and friction to make things slow nicely.
Look into quadratic graphs for easing to make things move smoothly over time, quadratic interpolation essentially.
I have an animation of a drinking glass filling up from empty to full, using CAKeyframeAnimation. The animation works perfectly fine.
I need to update a UILabel with the percentage corresponding to the fullness of the glass, i.e. it will read "0%" when the animation begins, and "100%" when it ends.
The keyframe animation runs for 5 seconds. I don't see a good way of updating this label. I can think of two options:
starting another thread and running a polling-type loop that updates the text at the cost of processing power.
break down the animation into 100 parts and use CABasicAnimation to update the text and then proceed with the next bit of glass animation.
Is there a better way to do this?
Thanks in advance
You can use either a NSTImer or dispatch_after() blocks to update the label at some scheduled interval:
// assume an ivar "NSTimer *myTimer" and "int count", initially 0
// fires 20 times, so update is 0.05
myTimer = NSTimer scheduledTimerWithTimeInterval:0.25 target:self selector:#selector(updateLabel:) userInfo:nil repeats:YES
- (void)updateLabel:(NSTimer *)timer
{
++count;
if(count >= 20) {
[timer invalidate];
}
CGFloat percent += 0.05;
myLabel.text = [NSString stringWithFormat:...
}
I was animating a ball bouncing on the screen moving an UIView by setting its origin property but I found that the movement of the ball was not smooth. I checked the frequency at which drawInRect was being called and it was 30 times/s, which I don't consider enough to make a fast ball movement smooth.
My question is, is it possible to ask the system to refresh/redraw the screen at 60fps? Can you do the same in OpenGL?
You might consider setting up a display link function. This will allow you to fire a method for every draw call that comes through the system (or every certain number of frames). The class you want to deal with is CADisplayLink, and here is a tutorial. If your animation is not fast enough with this still, then your animation is never going to look smooth ;).
You Don't need to change the fps of ios to get your animation right .You simply need to change it in YOu code , you can do it using NSTimer (An Example):-
Initialize a timer on Some Animation :-
float theInterval = 1.0/30.0;//Here you can set the frame rate , now set on 30
[NSTimer scheduledTimerWithTimeInterval:theInterval target:self
selector:#selector(animateView:) userInfo:nil repeats:YES];
- (void)animateView:(NSTimer *)theTimer
{
// your animation block here....
Something like..
view.center = CGPointMake(view.center.x+viewMovement.x,
view.center.y+viewMovement.y);
BOOL collision = view.center.y >= view2.center.y - 16 &&
view.center.y <= view2.center.y + 16 &&
view.center.x > view2.center.x - 32 &&
view.center.x < view2.center.x + 32;
if(collision)
viewMovement.y = -viewMovement.y
}
It'Just an example , implement your logic accordingly.