I have two UIViews with two different animations using UIView.animateWithDuration. The first animation starts right away, the second starts after a 0.5s delay.
How do I draw and animate a line between them like the below example:
My first attempt was to draw the line as a CGPath and then animate it using CABasicAnimation. This works if the two views (or shapes in that test) animates at the same time, not when the second animation has a delayed start.
I've then been looking into grabbing the values of the UIView frame positions on a continuous basis. That would enable me to redraw my line on each animation frame but I couldn't find any way of doing that either.
So... How do I achieve this?
CADisplayLink is probably what you are looking for.
Add an update method to your class and perform the animations there:
- (void)update {
// animate view 1
CGRect frame = view1.frame;
frame.origin.y += 1;
view1.frame = frame;
// animate view 2
// draw the line/animate another view
}
When you want to start the animation, do:
displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(update)];
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
Once the animation is complete remove the displayLink from the run loop.
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.
I want to make an animation that moves an UIImageView to another location on the screen of the device and simultaneously rotates the view.
I tried using UIView methods like transitionWithView and animateWithDuration and they work most of the time but not every time.
Randomly the animation seems to run so that the animation does not start from the current position but instead the UIImageView slides to its position from somewhere outside the screen. The ending point is correct and if I set the view transform without animation the end result is correct. So, it just looks like every now and then the starting transform for the animation is messed up.
I'm trying to work around this glitch by resetting the view transform before the animation with CGAffineTransformIdentity but even then I'm randomly seeing the view slide in from outside the screen. I tested this also on device and the same issue exists.
Here is the simplified code for the case. I'm reseting the transform to a fixed point on screen. Then I create a transform with a little slide up and rotation and in the animation I set this transform to the image view. The wait flag with run loop is there to make sure the execution does not continue from this call before the animation is finished.
-(void) foldAnimation:(int)player {
// reset view transform
UIImageView *leftCard = (UIImageView*)[self.view viewWithTag:6];
leftCard.transform = CGAffineTransformTranslate(CGAffineTransformIdentity, 120.0, 408.0);
// slide and rotate
float ySlide = -80.0 + arc4random_uniform(5);
CGAffineTransform transform1 = CGAffineTransformTranslate(leftCard.transform, 0.0, ySlide);
transform1 = CGAffineTransformRotate(transform1, M_PI/180*(arc4random_uniform(7)-2));
waitFlag = YES;
[UIView transitionWithView:leftCard
duration:0.4
options:UIViewAnimationOptionCurveEaseIn
animations:^{
leftCard.transform = transform1;
}
completion:^(BOOL finished){
waitFlag = NO;
}];
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (waitFlag && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
}
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 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.
I have a custom button which is my own subclass of UIButton. It looks like a circled arrow, starting at some angle startAngle end finishing at some endAngle=startAngle+1.5*M_PI.
startAngle is a button's property which is then used in its drawRect: method.
I want to make this arrow to continuously rotate by 2Pi around its center when this button is pressed. So I thought that I can easily use [UIView beginAnimations: context:] but apparently it can't be used as it doesn't allow to animate custom properties. CoreAnimation also doesn't suite as it only animates the CALayer properties.
So what is the easiest way to implement an animation of a custom property of UIView subclass in iOS?
Or maybe I missed something and it is possible with already mentioned techniques?
Thank you.
Thanks to Jenox I have updated animation code using CADisplayLink which seems to be really more correct solution than NSTimer. So I show the correct implementation with CADisplayLink now. It is very close to the previous one, but even a bit simpler.
We add the QuartzCore framework to our project.
Then we put the following lines in the header file of our class:
CADisplayLink* timer;
Float32 animationDuration; // Duration of one animation loop
Float32 initAngle; // Represents the initial angle
Float32 angle; // Angle used for drawing
CFTimeInterval startFrame; // Timestamp of the animation start
-(void)startAnimation; // Method to start the animation
-(void)animate; // Method for updating the property or stopping the animation
Now in implementation file we set the values for duration of the animation and the other initial values:
initAngle=0.75*M_PI;
angle=initAngle;
animationDuration=1.5f; // Arrow makes full 360° in 1.5 seconds
startFrame=0; // The animation hasn't been played yet
To start the animation we need to create the CADisplayLink instance which will call method animate and add it to main RunLoop of our application:
-(void)startAnimation
{
timer = [CADisplayLink displayLinkWithTarget:self selector:#selector(animate)];
[timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
This timer will call animate method every runLoop of the application.
So now comes the implementation of our method for updating the property after each loop:
-(void)animate
{
if(startFrame==0) {
startFrame=timer.timestamp; // Setting timestamp of start of animation to current moment
return; // Exiting till the next run loop
}
CFTimeInterval elapsedTime = timer.timestamp-startFrame; // Time that has elapsed from start of animation
Float32 timeProgress = elapsedTime/animationDuration; // Determine the fraction of full animation which should be shown
Float32 animProgress = timingFunction(timeProgress); // The current progress of animation
angle=initAngle+animProgress*2.f*M_PI; // Setting angle to new value with added rotation corresponding to current animation progress
if (timeProgress>=1.f)
{ // Stopping animation
angle=initAngle; // Setting angle to initial value to exclude rounding effects
[timer invalidate]; // Stopping the timer
startFrame=0; // Resetting time of start of animation
}
[self setNeedsDisplay]; // Redrawing with updated angle value
}
So unlike case with NSTimer we now don't need to calculate the time interval at which to update the angle property and redraw the button. We now only need to count how much time has passed from the start of animation and set the property to value which corresponds to this progress.
And I must admit that animation works a bit more smoothly than in case of NSTimer.
By default, CADisplayLink calls the animate method each run loop. When I calculated the frame rate, it was 120 fps. I think that it is not very efficient so I have decreased the frame rate to just 22 fps by changing the frameInterval property of CADisplayLink before adding it to mainRunLoop:
timer.frameInterval=3;
It means that it will call the animate method at first run loop, then do nothing next 3 loops, and call on the 4-th, and so on. That's why frameInterval can be only integer.
Thanks again to k06a for suggestion to use timer. I've made some study about working with NSTimer and now I want to show my implementation, since I think, it can be useful for others.
So in my case I had a UIButton subclass which was drawing a curved arrow which started from some angle Float32 angle; which is the main property from which the drawing of whole arrow starts. That means that just changing the value of angle will rotate whole arrow. So to make animation of this rotation I put the following lines in the header file of my class:
NSTimer* timer;
Float32 animationDuration; // Duration of one animation loop
Float32 animationFrameRate; // Frames per second
Float32 initAngle; // Represents the initial angle
Float32 angle; // Angle used for drawing
UInt8 nFrames; // Number of played frames
-(void)startAnimation; // Method to start the animation
-(void)animate:(NSTimer*) timer; // Method for drawing one animation step and stopping the animation
Now in implementation file I set the values for duration and frame rate of my animation and the initial angles for drawing:
initAngle=0.75*M_PI;
angle=initAngle;
animationDuration=1.5f; // Arrow makes full 360° in 1.5 seconds
animationFrameRate=15.f; // Frame rate will be 15 frames per second
nFrames=0; // The animation hasn't been played yet
To start the animation we need to create the NSTimer instance which will call method animate:(NSTimer*) timer every 1/15 seconds:
-(void)startAnimation
{
timer = [NSTimer scheduledTimerWithTimeInterval:1.f/animationFrameRate target:self selector:#selector(animate:) userInfo:nil repeats:YES];
}
This timer will call animate: method immediately and then repeat it every 1/15 second until it will be manually stopped.
So now comes the implementation of our method for animating a single step:
-(void)animate:(NSTimer *)timer
{
nFrames++; // Incrementing number of played frames
Float32 animProgress = nFrames/(animationDuration*animationFrameRate); // The current progress of animation
angle=initAngle+animProgress*2.f*M_PI; // Setting angle to new value with added rotation corresponding to current animation progress
if (animProgress>=1.f)
{ // Stopping animation when progress is >= 1
angle=initAngle; // Setting angle to initial value to exclude rounding effects
[timer invalidate]; // Stopping the timer
nFrames=0; // Resetting counter of played frames for being able to play animation again
}
[self setNeedsDisplay]; // Redrawing with updated angle value
}
The first thing I want to mention is that for me just comparison line angle==initAngle didn't work due to rounding effects. They don't are exactly the same after full rotation. That's why I check if they are just close enough and then set angle value to initial value to block small drift of angle value after many repeated animation loops.
And to be totally correct, this code must also manage conversion of angles to always be between 0 and 2*M_PI with something like this:
angle=normalizedAngle(initAngle+animProgress*2.f*M_PI);
where
Float32 normalizedAngle(Float32 angle)
{
while(angle>2.f*M_PI) angle-=2.f*M_PI;
while(angle<0.f) angle+=2.f*M_PI;
return angle
}
And another important thing is that, unfortunately, I don't know any easy way to apply easeIn, easeOut or other default animationCurves to this kind of manual animation. I think it doesn't exist. But it is, of course, possible to do it by hands. The line standing for that timing function is
Float32 animProgress = nFrames/(animationDuration*animationFrameRate);
It can be treated as Float32 y = x;, that means linear behavior, a constant speed, which is the same as speed of time. But you can modify it to be like y = cos(x) or y = sqrt(x) or y = pow(x,3.f) which will give some nonlinear behavior. You can think it yourself taking into account that x will go from 0 (start of animation) to 1 (end of animation).
For better looking code it is better to make some independent timing function:
Float32 animationCurve(Float32 x)
{
return sin(x*0.5*M_PI);
}
But now, since the dependence between animation progress and time is not linear, it's safer to use the time as indicator for stopping the animation. (You might want for example to make your arrow to make 1.5 full turns and than rotate back to the initial angle, that means your animProgress will go from 0 to 1.5 and than back to 1 while timeProgress will go from 0 to 1.)
So to be safe we separate time progress and animation progress now:
Float32 timeProgress = nFrames/(animationDuration*animationFrameRate);
Float32 animProgress = animationCurve(timeProgress);
and then check time progress to decide if should the animation stop:
if(timeProgress>=1.f)
{
// Stop the animation
}
By the way, if somebody knows some sources with list of useful timing functions for animation, I would appreciate if you share them.
Built in MacOS X utility Grapher helps a lot in visualizing the functions, so that you can see how your animation progress will depend on time progress.
Hope it helps somebody...
- (void)onImageAction:(id)sender
{
UIButton *iconButton = (UIButton *)sender;
if(isExpand)
{
isExpand = FALSE;
// With Concurrent Block Programming:
[UIView animateWithDuration:0.4 animations:^{
[iconButton setFrame:[[btnFrameList objectAtIndex:iconButton.tag] CGRectValue]];
} completion: ^(BOOL finished) {
[self animationDidStop:#"Expand" finished:YES context:nil];
}];
}
else
{
isExpand = TRUE;
[UIView animateWithDuration:0.4 animations:^{
[iconButton setFrame:CGRectMake(30,02, 225, 205)];
}];
for(UIButton *button in [viewThumb subviews])
{
[button setUserInteractionEnabled:FALSE];
//[button setHidden:TRUE];
}
[viewThumb bringSubviewToFront:iconButton];
[iconButton setUserInteractionEnabled:TRUE];
// [iconButton setHidden:FALSE];
}
}