4

Basically, what i'm trying to do is animate a cloud, and then change it's speed and/or direction mid-animation if the wind changes. If it matters, i am controlling the whole thing from a UIViewController, and the cloud exists of a UIView with a CALayer on top of that, which is the cloud itself. I have also tried it with a UIImageView on top.

For the TL:DR types, in short what i'm trying to do is either get the position of a animating view, or stop a animating view using block animations.

And here's the full story. My problem is to get its position during the animation. I am using block animations. Because i only know the speed with which it should move, i need to calculate the time myself by using the distance left. I have tried the following code, and several variations of it:

[[Cloud CloudImage]convertPoint:CGPointMake([[[Cloud CloudImage] presentationLayer] position].x, 0) toLayer:self.layer].x

Where Cloud is the UIView and CloudImage is the CALayer. This was the most complex variation i tried of this, i tried various simpler ones (with directly asking the Cloud, for example, or with UIView instead of CALayer). However, all it returns is its final value. I read something about this method being broken from 3.2, but being fixed in 4.2; However, it was not fixed when i changed the deployment target to iOS 4.3 instead of 4.0. I am using the 4.3 base sdk.

A few other variations i considered were stopping the animation alltogether for a moment, then getting the position and starting the new animation right away. However, i will need to know a way to stop a block-based animation in its tracks, and i have only found snippets for the older animation system (commitanimations).

The last one i considered was writing my own kind of animation system; The cloud would have a repeating NSTimer at 0.08 seconds or so, and create a 0.08 second core animation each time it fired, for which it uses the speed given to the cloud as a property. However, i fear that any variation of this will have much lower performance, while i need it to be as lightweight as possible, for i have up to 20 of these clouds simultaneously (and sometimes with rain too).

Thanks in advance!

Erik S
  • 1,939
  • 1
  • 18
  • 44

1 Answers1

7

I definitely would roll my own system in this case, and the performance loss versus using built-in animation code can be minimized using CADisplayLink rather than NSTimer (CADisplayLink is directly tied to the on-screen updates timer).

The fact that you have 20 clouds simultaneously doesn't really change things too much, as I presume your intention is to animate those 20 clouds separately using the built in animations already.

If you're unsure of how big of a performance hit it will have on things in the end, you could try simply adding a bunch of clouds (50 or so) using the built in animation and see how sluggish they move, then switching it out to built-in animation code and comparing.

Edit: this discussion on Stack Overflow goes into detail on how to do what you're asking, in case you go that route: Cancel a UIView animation?

Example:

// startAnimating called once to initiate animation loop. might be stopped e.g. 
// if game is paused or such
- (void)startAnimating
{
    // displayLink = the CADisplayLink for the current animation, if any.
    if (displayLink) {
        [displayLink invalidate]; 
        [displayLink release];
    }

    displayLink = [[CADisplayLink displayLinkWithTarget:self
                                               selector:@selector(animationTick:)] 
                   retain];

    [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

// tick method is called at every interval; we want to update things based on 
// a delta time (duration), so that if things get bogged down and the updates 
// come less often, we don't go into slow motion
- (void)tick:(CADisplayLink *)sender
{
    CFTimeInterval duration = sender.duration;
    // here, we update the position for all the UIView objects. example:
    CGRect cloudFrame;
    for (UIView *cloud in clouds) {
        cloudFrame = cloud.frame;
        cloudFrame.origin.x += windForceX * duration;
        cloudFrame.origin.y += windForceY * duration;
        cloud.frame = cloudFrame;
    }
    // here we might update the windForceX and Y values or this might happen somewhere
    // else
}
Community
  • 1
  • 1
Kalle
  • 13,186
  • 7
  • 61
  • 76
  • Awesome answer, thanks! Never heard of CADisplayLink before, but it sounds like exactly what i need. The reason i feared for the 20 clouds was because it would multiply any performance hit 20x. – Erik S Mar 18 '11 at 17:24
  • The more I think about it, the more I am convinced that rolling your own animation system is much better performance wise, presuming you update all the clouds all at once using one single timer (instead of having one timer for every cloud). Presuming the clouds move in the same direction and such, this is probably what you WANT to do regardless. – Kalle Mar 18 '11 at 18:47
  • Instead of CADisplayLink you mean? I have it working, but when it's raining it lags like hell. I set the selector to add the windspeed*timestampDifference to its horizontal position, but when it's raining (when the cloud creates a bunch of UIImageViews and animates them to fall), it becomse WAY too sluggish, like updating only every second. It does, however, update all the way to where it should've been, so i suspect CADisplayLink to call slower. Will try with my own timer now. – Erik S Mar 18 '11 at 18:57
  • No no, you definitely want to use CADisplayLink. What I'm saying is, you don't want one CADisplayLink running for every cloud, you want one "central" CADisplayLink which updates all the clouds all at once. – Kalle Mar 18 '11 at 19:00
  • Ah, alright. I had set all different displaylinks indeed, will try if it runs smoothly if i use a central link. Does it matter which mode i use btw? – Erik S Mar 18 '11 at 19:03
  • Yep, this is almost exactly what i have made now! I just have to make sure that i get one central rain timer instead of one on each cloud, i didnt realise that NSTimer was so resource-heavy. The only difference is that i save the displaylink as a instance variable, as i will only start it once, and will only have one at a time. Is that a bad habit? – Erik S Mar 19 '11 at 09:31
  • No, that's how I do it above too. displayLink is an instance variable. With the above code, if you ever decided to restart the displayLink, you wouldn't leak the old one (hence the invalidate/release part). – Kalle Mar 19 '11 at 09:59
  • Ah, didn't notice you weren't declaring it again. I should actually get that habit too, i suppose. – Erik S Mar 19 '11 at 14:35