I'm moving around image views with timers and animation blocks. At the same time a user can click on a few of these views(buttons) and then I have some more animation blocks that start moving/resizing/rotating different image views.
Changing alpha of views and giving out new CGpoint to views works very well for me but calling resizing or rotating with the uiview.transform function seems to reset all my views to their starting position(from the storyboard).
I really would love to be able to use this transform function inside an animation block but if I can't somehow bypass this reset it is not really a viable option for me.
example code:
- (void)moveGust:(NSTimer*) timer
{
if(centerCloud1.x >= 1100)
{
centerCloud1.x = -79;
gustOutlet.center = centerCloud1;
}
if(centerCloud2.x >= 1100)
{
centerCloud2.x = -79;
cloudOutlet.center = centerCloud2;
}
centerCloud1 = gustOutlet.center;
centerCloud1.x = round(centerCloud1.x+10);
centerCloud2 = cloudOutlet.center;
centerCloud2.x = round(centerCloud2.x+8);
[UIView animateWithDuration:2 delay:0 options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionCurveLinear animations:^(void){
gustOutlet.center = centerCloud1;
cloudOutlet.center = centerCloud2;
}completion:^(BOOL Finished){ }];
}
- (IBAction)signAction:(id)sender {
[UIView animateWithDuration:0 animations:^(void) {
signTop.transform = CGAffineTransformScale(signTop.transform, 1.1, 1.1);
}];
}
Any ideas?
Thanks!
Successive transforms need to be concatenated using CGAffineTransformConcat().
When you set the view's transform equal to a new transform, you effectively "undo" previous transforms. To do a series of transforms, concatenate them with the above method.
Related
I have two views: A and B. A is positioned at the top of the screen, B is positioned at the bottom of the screen.
When the user presses a button, view B animates upwards with a EaseInEaseOut bezier curve until it reaches y = 0. While B is on its way to its destination, it should push A up when it hits A. In other words, when B has passed a certain y coordinate (A's y origin + height) during its transition from bottom to top, A should stick to B so it seems B pushes A upwards.
What I have tried so far:
Register a target + selector to a CADisplayLink immediately after the user pressed the button. Inside this selector, request view B's y coordinate by accessing its presentationLayer and adjust A's y coordinate accordingly. However, this method turns out to be not accurate enough: the presentationLayer's frame is behind on B's current position on the screen (this is probably because -presentationLayer recalculates the position of the animating view on the current time, which takes longer than 1 frame). When I increase B's animation duration, this method works fine.
Register a target + selector to a CADisplayLink immediately after the user pressed the button. Inside this selector, calculate B's current y coordinate by solving the bezier equation for x = elapsed time / animation duration (which should return the quotient distance traveled / total distance). I used Apple's open source UnitBezier.h for this (http://opensource.apple.com/source/WebCore/WebCore-955.66/platform/graphics/UnitBezier.h). However, the results are not correct.
Any suggestions on what I can try next?
Two simple solutions:
Use animationWithDuration only: You can break your animation into two nested animations, using "ease in" to animate the moving of "B" up to "A", and then using "ease out" to animate the moving of "B" and "A" the rest of the way. The only trick here is to make sure the two duration values make sense so that the speed of the animation doesn't appear to change.
CGFloat animationDuration = 0.5;
CGFloat firstPortionDistance = self.b.frame.origin.y - (self.a.frame.origin.y + self.a.frame.size.height);
CGFloat secondPortionDistance = self.a.frame.size.height;
CGFloat firstPortionDuration = animationDuration * firstPortionDistance / (firstPortionDistance + secondPortionDistance);
CGFloat secondPortionDuration = animationDuration * secondPortionDistance / (firstPortionDistance + secondPortionDistance);
[UIView animateWithDuration:firstPortionDuration
delay:0.0
options:UIViewAnimationOptionCurveEaseIn
animations:^{
CGRect frame = self.b.frame;
frame.origin.y -= firstPortionDistance;
self.b.frame = frame;
}
completion:^(BOOL finished) {
[UIView animateWithDuration:secondPortionDuration
delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
CGRect frame = self.b.frame;
frame.origin.y -= secondPortionDistance;
self.b.frame = frame;
frame = self.a.frame;
frame.origin.y -= secondPortionDistance;
self.a.frame = frame;
}
completion:nil];
}];
You can let animateWithDuration handle the full animation of "B", but then use CADisplayLink and use presentationLayer to retrieve B's current frame and to adjust A's frame accordingly:
[self startDisplayLink];
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
CGRect frame = self.b.frame;
frame.origin.y = self.a.frame.origin.y;
self.b.frame = frame;
}
completion:^(BOOL finished) {
[self stopDisplayLink];
}];
where the methods to start, stop, and handle the display link are defined as follows:
- (void)startDisplayLink
{
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopDisplayLink
{
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
CALayer *presentationLayer = self.b.layer.presentationLayer;
if (presentationLayer.frame.origin.y < (self.a.frame.origin.y + self.a.frame.size.height))
{
CGRect frame = self.a.frame;
frame.origin.y = presentationLayer.frame.origin.y - self.a.frame.size.height;
self.a.frame = frame;
}
}
You said that you tried the "animate B and use display link to update A" technique, and that it resulted in "A" lagging behind "B". You could theoretically animate a new view, "C", and then adjust B and A's frames accordingly in the display link, which should eliminate any lag (relative to each other).
You can try followin algorythm:
1)Put A and B in UIView(i.e UIview *gropedView)
2)Change B.y till it will be equal A.y+A.height(so B will be right under the A)
3)Animate groupedView
I'm using animateWithDuration to have one of my UIVIew rotating endlessly. Now, I need to get the current value of my UIView's rotation at a specific time.
I've tried to use the following function :
-(float)getCurrentRotation{
CGAffineTransform transform = ((UIImageView*)[self.subviews objectAtIndex:0]).transform;
return (atan2(transform.b, transform.a));
}
but it always returns the same value (M_PI/2) as it's the value I've specified in my initial call to animateWithDuration :
[UIView animateWithDuration:4 delay:0 options:( UIViewAnimationOptionRepeat|UIViewAnimationOptionCurveLinear) animations:^{
CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI/2);
((UIImageView*)[self.subviews objectAtIndex:0]).transform = transform;
}
completion:^(BOOL finished){
}];
Is there a way to have the current value of the rotation ?
Thanks.
When using CoreAnimation based animation, your view properties (bounds, transform...) never actually change. What changes are the properties of the layer displayed on screen while animating (see this documentation page), which is accessible using view.layer.presentationLayer.
Thus, the following line will get you the actual transform at the time you ask for it:
CGAffineTransform transform = CATransform3DGetAffineTransform([(CALayer*)[[[self.subviews objectAtIndex:0] layer] presentationLayer] transform]);
You must link against and include the QuartzCore framework, which defines CALayer, for this to work.
#import <QuartzCore/QuartzCore.h>
I'm not sure if it's possible to watch the "value of the rotation" while it is shown on screen. But I'd say it is not, because your view object does have the new property value immediately and the animation takes place somewhere deep inside the framework on objects you're not able to reach.
To fulfill your goal I'd separate the rotation into single steps with a granularity sufficient for your needs. Then just count the steps and make the current step available. Could look a bit like this (not tested, just a scribble):
//step and rotator declared as class members
//rotator is the one which is referred to as ((UIImageView*)[self.subviews objectAtIndex:0]) in the original question
-(CGFloat)currentRotation
{
return step*(M_PI/16);
}
-(void)turnturnturn
{
[UIView animateWithDuration:0.2
animations:^
{
rotator.transform = CGAffineTransformConcat(CGAffineTransformMakeRotation(M_PI/16), rotator.transform);
step++;
}
completion:^(BOOL finished)
{
if(step == 32)
{
step = 0;
rotator.transform = CGAffineTransformIdentity;
}
[self turnturnturn];
}];
}
I can animate something across the screen like so:
aView.frame = CGRectMake(100,100,50,50);
[UIView animateWithDuration:25.0 delay:0.0 options:UIViewAnimationCurveLinear animations:
^{
aView.frame = CGRectMake(200,100,50,50);
}
completion:^(BOOL finished)
{
aView.hidden = YES;
}
];
..but how do I get the position of the item while it is animating? If I put a button on the screen and use this action
-(IBAction)buttonTapped:(id)sender
{
NSLog(#"x pos:%f", aframe.origin.x)l
}
then all I get from this is
x pos:200.00
..which is the final position of the item, but not the current. How do I get the current?
Look at CALayer's presentationLayer property. It holds the current values of the layer being animated.
You will have to import QuartzCore framework.
EDIT
Specifically:
#import <QuartzCore/CALayer.h>
// ...
CGPoint inMotionPosition = myView.layer.presentationLayer.position;
I'm animating an UIView and want to check if its frame intersects with another UIView's frame. This is how I "spawn" one of the UIViews:
- (void) spawnOncomer
{
oncomer1 = [[Oncomer alloc] initWithType:#"car"];
[self.view addSubview:oncomer1];
//make the oncomer race across the screen
[UIView beginAnimations:nil context:NULL]; {
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:3.0];
[UIView setAnimationDelegate:self];
CGRect f = oncomer1.frame;
f.origin.y = self.view.frame.size.height+oncomer1.frame.size.height;
oncomer1.frame = f;
[UIView setAnimationDidStopSelector:#selector(decountCar)];
}
[UIView commitAnimations];
}
So far so good. Now I want to check if this UIView and my other UIView collide by doing this:
- (void) checkCollision {
bool collision = CGRectIntersectsRect(car.frame, oncomer1.frame);
if (collision) {
NSLog(#"BOOOOOOOM");
} else {
NSLog(#"Oncomer: %#", NSStringFromCGRect(oncomer1.frame));
}
}
However, they never collide. Although I see oncomer1 moving across the screen, loggin oncomer1.frame never changes: it keeps outputting Oncomer: {{50, 520}, {30, 60}}(which are the post-animation values).
Does anyone know why this is?
P.s. Both methods are called directly or indirectly with a NSTimer and are thus performed in the background
UIView geometry updates apply immediately to their CALayer, even in an animation block. To get a version of a layer with animations applied, you can use -[CALayer presentationLayer], like this (warning - untested code):
- (void) checkCollision {
CGRect oncomerFrame = oncomer1.layer.presentationLayer.frame;
bool collision = CGRectIntersectsRect(car.frame, oncomerFrame);
if (collision) {
NSLog(#"BOOOOOOOM");
} else {
NSLog(#"Oncomer: %#", NSStringFromCGRect(oncomerFrame));
}
}
From your code:
CGRect f = oncomer1.frame;
f.origin.y = self.view.frame.size.height+oncomer1.frame.size.height;
oncomer1.frame = f;
A logical explanation of the frame never changing is that you are only changing the y of the frame, and you are always setting it to the same value determined by two heights.
I was wondering how I can set an animation to repeat. The number of repetitions needs to be determined by a variable. In the following code, the variable int newPage should determine how often the animation is repeated.
I tried this, but the animation (which employs a block animation) was only executed once:
for (int temp = 1; temp <= newPage; temp++) {
[self animatePage];
}
If I code the following, it works like I want it to, but this is hardcoded (i.e. the animation will be repeated twice) and I can't see a way of how to change the number of how often this animation is executed in code and according to my variable newPage:
[UIView animateWithDuration:0
delay:0.1
options:UIViewAnimationOptionCurveEaseIn
animations:^{[self animatePage];}
completion:^(BOOL finished){[self animatePage];}];
I'd be very grateful for suggestions of how to repeat the same animation without having to hardcode the number of times I want this animation to be repeated.
EDIT:
I tried to implement the following code, but only one animation will actually be carried out:
[UIView animateWithDuration:0
delay:1
options:UIViewAnimationOptionCurveEaseIn
animations:^{
[UIView setAnimationRepeatCount:2];
[self animatePage];
}
completion:nil];
Had the same problem - you were missing an an'UIViewAnimationOptionRepeat'
This should work:
[UIView animateWithDuration:0
delay:1
options:UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionRepeat
animations:^{
[UIView setAnimationRepeatCount:2]; // **This should appear in the beginning of the block**
[self animatePage];
}
completion:nil];
Did the trick for me.
Did you try to set the repeatCount? + (void)setAnimationRepeatCount:(float)repeatCount
I've tried the following code block, and it definitely repeats 2x for me (l was a UITextView that was scaled up by 2x in X dir and 3X in Y dir):
[UIView animateWithDuration:2
delay:0.1
options:UIViewAnimationOptionCurveEaseIn
animations:^{ [UIView setAnimationRepeatCount:2];
l.transform = CGAffineTransformMakeScale(2,3); } completion:nil];
the reason you arent seeing but one animation is that due to the fact that you are calling the animation from a loop in the same runloop, resuting in the last call winning (one animation)
instead of calling [self animatePage], try calling
[self performSelector:#selector(animatePage) withObject:nil afterDelay:.1 *temp];
this will create your calls on separate threads.
you may need to play with the delay interval
To avoid the hiccup between animations, use keyframes. I wrote an extension that works on anything conforming to UIView (which is a ton of visual elements)
Swift 4:
import UIKit
extension UIView{
func rotate(count: Float, _ complete: #escaping ()->()) {
UIView.animateKeyframes(withDuration: 1.0, delay: 0, options: [.repeat], animations: {
//to rotate infinitely, comment the next line
UIView.setAnimationRepeatCount(count)
//rotate the object to 180 degrees
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5/1.0, animations: {
self.transform = CGAffineTransform(rotationAngle: (CGFloat(Double.pi)))
})
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5/1.0, animations: {
//rotate the object from 180 to 360 degrees
self.transform = CGAffineTransform(rotationAngle: (CGFloat(Double.pi * 2)))
})
}, completion:{ _ in
complete()
})
}
}
To call it anywhere in the app:
view.rotate() { /*do after*/ }