I'm simulating some effects, like leafs falling, snow and rain.
My first call was to use CAEmitterLayer, that works very well, but i need to render the layer in context to save as an image, and that seens impossible or, at least, very complicated.
So i am working with some view animations. I need to continuously animate my imageViews so it can fall through screen and reappear on top of it, each one with diffenrent speed.
So i relocate each imageView on screen with animateWithDuration and when animation is done i call the method that do that recursively from completion block so the imageview can make it's way to the end of screen. Once an imageView reachs the end of screen i relocate it on top.
The problem is when animation is over my imageView stops a little bit until the completion block calls the method recursively, i want it to be continuously.
My code generates random position for each imageView, and my animation duration is very short so i can always update imageView's location in case the user tap the button to save the view as an image.
Anyway, here is my code:
- (void)effectOnObject:(UIImageView *)object{
NSInteger initialX;
NSInteger initialy;
CGFloat duration;
//the object have reached the end of screen
if (object.frame.origin.y >= self.frame.size.height) {
//generate random position to relocate the object on x axis
initialX = arc4random() % 321;
//generate random position to relocate the object on y axis (little bit above top of screen)
initialy = ((NSInteger)-object.frame.size.height) - arc4random() % 11;
//duration 0 so the object can be replaced without animation
duration = 0.0;
}
else{
initialX = object.frame.origin.x;
//this change the speed of object
initialy = object.frame.origin.y + 10
//setted duration to short time
duration = 0.01;
}
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionAllowAnimatedContent
animations:^{
[object setFrame:CGRectMake(initialX, initialy, object.frame.size.width, object.frame.size.height)];
}
completion:^(BOOL completed){
//recursively call
[self effectOnObject:object];
}];
}
- (void)startEffect{
//initiate effect on each imageView
for (UIImageView *object in self.subviews) {
[self effectOnObject:object];
}
}
How can i make this animation repeatedly and continuously, without the gap when the method is called recursively from completion block and the animation restarts?
You don't need to do the animation with 0 duration when the object reaches the end of the screen. Just test for this in your completion block and relocate the view before calling recursively. Also, a duration of 0.01 seems a bit extreme (100 frames per second). Perhaps a more reasonable value like 0.1 would help.
Something like this:
- (void)effectOnObject:(UIImageView *)object{
//setted duration to short time
CGFloat duration = 0.1;
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionAllowAnimatedContent
animations:^{
NSInteger initialX = object.frame.origin.x;
//this change the speed of object
NSInteger initialy = object.frame.origin.y + 10;
[object setFrame:CGRectMake(initialX, initialy, object.frame.size.width, object.frame.size.height)];
}
completion:^(BOOL completed){
NSInteger initialX = arc4random() % 321;
//generate random position to relocate the object on y axis (little bit above top of screen)
NSInteger initialy = ((NSInteger)-object.frame.size.height) - arc4random() % 11;
[object setFrame:CGRectMake(initialX, initialy, object.frame.size.width, object.frame.size.height)];
//recursively call
[self effectOnObject:object];
}];
}
From what I read briefly you should check out CAReplicatorLayer. It is perfect for repeating stuff! Only issue I can think of is that the transform and values have to be incremental...
Related
I'm trying to increment a percentage view using the following code:
// Progress
[UIView animateWithDuration:5.0 animations:^{
for (float percent = 0.0; percent <= 0.86; percent+=0.01) {
NSLog(#"%f", percent);
self.circularProgress2.progress = percent;
}
}];
It doesn't animate, it goes straight to the final value. I know I can use NSTimer:
[NSTimer scheduledTimerWithTimeInterval:0.0001 target:self selector:#selector(onLableAnimateTick:) userInfo:nil repeats:YES];
And then change the value from inside the called method. But I have several bars on the same view, so I guess using the first option would be better for me.
If not possible, how would I duplicate the NSTimer behavior without having to create properties and methods for every progress bar?
All the code inside the animation-block will be executed which is your case is a for-loop. Consider the following case:
[UIView animateWithDuration:5.0 animations:^{
CGRect f = aView.frame;
f.origin.x = 20;
f.origin.x = 25;
f.origin.x = 40;
aView.frame = f;
}];
This will animate the view from it's current position to x = 40. Not animate to 20, then to 25, then to 40. The result of the block is that aViews frames origin.x should be 40, no matter what happened before that.
I'd say use a timer if you want to auto-increment the progress with a given interval. You can still animate the change of course (for example by overriding the setProgress-method on your Progress-view class and call an animation from there), but don't use an animation block to increment the model value (progress).
I have 9 custom subviews that I create and lay out programmatically. I want to animate their creation and change of position each one at a time but right now the whole set of 9 subviews is being animated at the same time.
This is my code:
- (void)viewDidLoad{
//create 9 setCardViews and add them to the array setCards to be able to identify them later
for (int i=0; i<9; i++) {
SetCardView *card = [[SetCardView alloc]initWithFrame:CGRectMake(self.gameView.center.x, self.gameView.center.y, 0, 0)];
[self.gameView addSubview:card];
[self.setCards addObject:card];
}
//layout those 9 views
[self updateLayout];
}
Then, the updateLayout method is called. It sets a frame size inside of which the subviews are laid out:
-(void)updateLayout
{
CGRect canvas = [self resizeCanvasWithNumberOfCards:[self.setCards count]];
for (SetCardView *card in self.setCards)
{
[self layOutCard:card withFrame:canvas atIndex:[self.setCards indexOfObject:card]];
}
}
Finally, inside the layOutCard method I calculate the position of the view (I omit that part of the code here) and I animate the change in its frame:
-(void)layOutCard:(SetCardView *)card withFrame:(CGRect)canvas atIndex:(NSUInteger)index
{
//calculate the new frame of the view
CGRect rect = //calculations ;
[SetCardView animateWithDuration:0.5 delay:self.delay options: UIViewAnimationOptionCurveEasyInOut animations:^{card.frame=rect;} completion:nil];
}
So all these animations happen at the same time, maybe because it's inside a loop in the updateLayout method so the controller waits for it to finish? Anyway I can't think of a way to make the animation of the views without using a loop so that they animate one at a time.
How could I do that?
Thanks
One of the way you can make it works is increase delay when you enumerate cards in updateLayout and after that pass it to layOutCard:withFrame:atIndex (of course you have to add delay parameter to that method. Or ft index parameter start from 0 and increase by 1 every time you can use it to calculate delay:
-(void)layOutCard:(SetCardView *)card withFrame:(CGRect)canvas atIndex:(NSUInteger)index
{
//calculate the new frame of the view
CGRect rect = //calculations ;
float myDelay = 0.5 * index; //0.5 is your duration.
[SetCardView animateWithDuration:0.5 delay: myDelay options: UIViewAnimationOptionCurveEasyInOut animations:^{card.frame=rect;} completion:nil];
}
You have it done (almost). One small adjustment on your code. At a run time your for loop is executed very fast leaving you with the impression that all animations are starting at the same time. You are using start with delay on your animation block already so simply increase the delay every time you go into to loop like this.
-(void)updateLayout
{
CGRect canvas = [self resizeCanvasWithNumberOfCards:[self.setCards count]];
for (SetCardView *card in self.setCards)
{
[self layOutCard:card withFrame:canvas atIndex:[self.setCards indexOfObject:card]];
// increase the delay so next animation will start after this one is finished
// check if this card is the last from your array
if([card isEqual:[self.setCards lastObject]])
{
// if card is last object set self.delay back to 0
self.delay = 0;
{
else
{
// if card is not the last object increase the delay time for next animation
self.delay += 0.5; // your current animation duration
}
}
}
I'm trying to make an arm wave effect (Hello!) with UIView animations, but it snaps back to the beginning when the last one goes off. I want the wave to go back and forth.
First keyframe: Rotation 30˚
Second keyframe: Rotation -30˚
Third keyframe: SHOULD BE Rotation 0˚
arm.layer.anchorPoint = CGPointMake(1.0, 1.0);
float x = arm.frame.origin.x + arm.frame.size.width;
float y = arm.frame.origin.y + arm.frame.size.height;
arm.layer.frame = CGRectMake(x, y, arm.frame.size.width, arm.frame.size.height);
[self.scrollView arm];
float degrees = 30.0;
float radians = (degrees/90.0)*M_PI;
[UIView animateKeyframesWithDuration:4.0f delay:0.0 options:UIViewKeyframeAnimationOptionRepeat
animations:^{
[UIView addKeyframeWithRelativeStartTime:0 relativeDuration:.5 animations:^{
arm.transform = CGAffineTransformMakeRotation(radians);
}];
[UIView addKeyframeWithRelativeStartTime:.5 relativeDuration:.75 animations:^{
arm.transform = CGAffineTransformMakeRotation(-radians);
}];
[UIView addKeyframeWithRelativeStartTime:1.25 relativeDuration:.5 animations:^{
arm.transform = CGAffineTransformMakeRotation(0);
}];
The reason is that you used up the whole animation on the first two frames of the keyframe animation. These are relative times and relative durations; they must all be less than 1!
So, on the first one you use up half the animation (.5).
On the second one you start halfway through the animation (.5) and then you use up the entire rest of the animation (.75 is larger than .5 which is all you've got left).
So the third one never happens. And in any case it's completely nuts: you can't have a relative start time of 1.75 because 1 is the end of the animation. Read the docs:
This value must be in the range 0 to 1, where 0 represents the start of the overall animation and 1 represents the end of the overall animation. For example, for an animation that is two seconds in duration, specifying a start time of 0.5 causes the animations to begin executing one second after the start of the overall animation.
I've got a view which is a 4x4 grid of images, and am trying to achieve the effect of every square, going from top to bottom and left to right, flipping over to reveal a new image. Should be a bit of a "ripple" effect, so the top left square starts flipping immediately, the [0,1] square flips after 0.05 seconds, [0,2] after 0.10 seconds, and so on, so that after (0.05*15) seconds the final square starts to flip.
My implementation is this: Each grid square is a UIView subclass, DMGridSquareView, which contains a UIImageView, and the following method:
- (void)flipToNewImage:(UIImage*)img {
AATileView *mySelf = self;
[UIView transitionWithView:self.imageView
duration:0.4
options:UIViewAnimationOptionTransitionFlipFromLeft animations:^{
self.imageView.image = img;
} completion:^(BOOL finished) {
NSLog(#"Finished flip");
}];
}
Then, in order to trigger the animation of all the grid squares, within the DMGridView, I call this method:
- (void)flipAllSquares:(NSArray*)newImages {
int numTiles = self.numColumns * self.numRows;
for (int idx=0; idx<numTiles; idx++) {
UIImage *newImg = [newImages objectAtIndex:idx];
[[self.gridSquareViews objectAtIndex:idx] performSelector:#selector(flipToNewImage:) withObject:newImg afterDelay:(0.05*idx)];
}
}
When running on the simulator, this works beautifully. But when running on my iPhone 5, it has an unexpected (to me, at least) behavior. The animations of the individual grid squares are nice and smooth, and at the correct speed, but the start times are off. They happen in staggered "chunks". So the first 2 squares might flip immediately, then the next 4 squares will simultaneously flip after a moment, then the next 3 flip after another moment, and so on. It's not consistent, but it's always in chunks like this.
Any idea how to fix this?
Thanks.
EDIT 1:
Sorry, it looks like I misunderstood your question. You do not want them to flip one after another, you would like each view to begin flipping very soon after the previous view has begun flipping.
Have you tried calling the method recursively from the completion block? This way you know the method is only being called after the previous image has completed the flip animation.
NSInteger imageFlipIndex = 0;
- (void)flipToNewImage:(UIImage*)img {
AATileView *mySelf = self;
[UIView transitionWithView:self.imageView
duration:0.4
options:UIViewAnimationOptionTransitionFlipFromLeft animations:^{
self.imageView.image = img;
} completion:^(BOOL finished) {
imageFlipIndex++;
if (imageFlipIndex < newImages.count) {
UIImage *newImg = [newImages objectAtIndex:idx];
[self flipToNewImage:newImg];
} else {
imageFlipIndex = 0;
}
NSLog(#"Finished flip");
}];
}
What happens if you try:
imageFlipIndex++;
if (imageFlipIndex < newImages.count) {
UIImage *newImg = [newImages objectAtIndex:idx];
[[self.gridSquareViews objectAtIndex:imageFlipIndex] performSelector:#selector(flipToNewImage:) withObject:newImg afterDelay:(0.05*idx)];
} else {
imageFlipIndex = 0;
}
inside the animations block?
just started to use Obj C for iOS, I was trying to animate all the views (UIImageView) inside my subviews, but when I set the delay time to less than 1 second, the animation would animate all views at once. If i set the delay to 1 second or more, it will be animate correctly but the second animation will only start after the first one completes. I want to animate all those views back to back for example view number 1 would start animating, before it completes, the second should start to animate.
Here are my code.
-(void)animate
{
int delaytime = 0.3; //first view will start after 0.3 secs
for(UIView *view in self.subviews)
{
view.alpha = 0; //set it to transparent
delaytime += 1 ; //couldn't get the desired effect when it is less than 1
[UIView animateWithDuration:0.3
delay:delaytime
options: UIViewAnimationOptionTransitionNone
animations:^{
view.alpha = 1; // fade in }
completion:^(BOOL finished){
NSLog(#"Complete");
}];
}
}
Your delay variable is an int and is therefore always rounded down to the integer value (0.3 becomes 0 and even 0.9999 becomes 0). This is the reason why all "delays" less than one second starts immediately for you, an int variable can't hold decimal values.
You should instead us the correct type NSTimeInterval
NSTimeInterval delaytime = 0.3;