"Push" animation in Objective-C - iphone

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

Related

Transform resets(redraws) all my former moved views

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.

move image on tilt within boundaries

I'm currently creating a iPhone app that displays my speed heading location and a few other sensor readings for use on my bike. I've managed to get the tilt reading from the accelerator. what I'm trying to do is move an image either left or right depending on what the tilt reading is.
_______________________________
: === :
: = = :
: === :
-------------------------------
lets say the = signs is my image, I'm trying to get it to move either towards the left or right edge depending on the tilt angle.
any ideas? I've managed to move it by animations but I don't want it to continuously move if I'm leaning for a long period of time.
First I added the accelerator data using:
UIAccelerometer *theAccelerometer = [UIAccelerometer sharedAccelerometer];
theAccelerometer.updateInterval = 0.1; //in seconds
theAccelerometer.delegate = self;
Notice the timing is 0.1 seconds. You will want to match that timing to the animation below. I then set the animation up in the accelerometer:didAccelerate: delegate call:
-(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
NSInteger newX = 0;
newX = imageView.frame.size.width/2 + ((320-imageView.frame.size.width) * (acceleration.x+1)/2.0);
[UIView animateWithDuration:0.1 animations:^{
imageView.center = CGPointMake(newX, imageView.center.y);
}] ;
}
It is important to match the animation duration with the updateInterval of the UIAccelerometer so that you make sure to complete the animation before the next update comes through.
The important part here is probably the math to determine the newX position. I basically took half of the width of the image, because that is the left most absolute position. From there I knew I had a 320-imageView.frame.size.width amount of room that I could add. If I added the entire 320-imageView.frame.size.width then I would be at 320-imageView.frame.size.width/2 which is the right-most position. From there I just needed to break up the 320-imageView.frame.size.width chunk according to how much we have accelerated. The accelerator.x data is in the range -1 to 1, so my entire range is 2. So I added 1 to the accelerator.x to normalize it from 0-2. Then I divided by 2.0 so that I would have a range from 0-1. Now I had a multiplier that I could multiply my 320-imageView.frame.size.width range by. If it was 0 I would be at the left-most position. If it was 1 I would be at the right-most position, and anything in between would be broken up linearly. So the final term is:
imageView.frame.size.width/2 + ((320-imageView.frame.size.width) * (acceleration.x+1)/2.0);
to get the new x position of the image.
I hope that is what you are looking for! I tested it on a device and it is pretty smooth as long as you match up your durations.
If you are asking the question about animation. This is code apple recommnad to animate UIView. And you can change value for x and y coordinate.
imageFrame.origin.y = 200;
[UIView animateWithDuration:0.5
delay:0.0
options: UIViewAnimationCurveEaseOut
animations:^{
datePickerView.frame = imageFrame;
}
completion:^(BOOL finished){
self.datePickerViewFlag = YES;
}];

CGRect not changing during animation

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.

Dragging a view within a view, constraining it within

I have a pretty simple bit of code for dragging and drawing.
Three views: The root view is basically the window (with a few interface controls).
Within it, I have a "canvas" view. Within the canvas view, I have a third view (a small graphic of a character), which the user is meant to "drag" around within the canvas. The dragging works fine.
The problem is that I can't seem to constrain my "character" within the bounds of the canvas. Ideally, when he runs into the edge of the canvas, he should stop (not spill over into the root view)
The canvas view clips subviews; so he does disappear once he goes over the edge - but I don't want him to disappear, I want him to not be able to go any further.
I also call CGRectIsWithinRect before my animation, which in principle should do the trick. The problem is that the frame of the character isn't updated while the animation is occurring - so as long as he's already moving, he'll keep on moving right of the side of the page.
-(void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UIView * charView = [[canvasView floatingView] retain];
CGPoint pt = [[touches anyObject] locationInView:canvasView];
if (CGRectContainsPoint([canvasView bounds], pt)) {
CGRect charFrame = charView.frame;
CGRect containerFrame = canvasView.frame;
BOOL inTheYard= CGRectContainsRect(containerFrame, charFrame);
if (inTheYard == NO) {
//if we're at the edge, don't go any further
return;
}
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:.2];
CGFloat x = floatingView.center.x + (pt.x - startPoint.x);
CGFloat y = floatingView.center.y + (pt.y - startPoint.y);
CGPoint middle = CGPointMake(x,y);
floatingView.center = middle;
[UIView commitAnimations];
[floatingView release];
}
}
How can I keep him from moving past the edges of his superview?
Instead of checking if the touch event is inside the canvas you can clamp it to be inside. Check the x and y points for the touch event separately and if they are outside the canvas move the character up to the wall on that side of the canvas.
CGFloat w = canvasFrame.frame.size.width;
CGFloat h = canvasFrame.frame.size.height;
if (pt.x < 0) pt.x = 0;
if (pt.x > w) pt.x = w;
if (pt.y < 0) pt.y = 0;
if (pt.y > h) pt.y = h;
This will allow you to keep dragging the character along the wall of the canvas even when the touch event is outside (if you remove the inTheYard check).

Performance issue with DrawRect and NSTimer

I'm trying to make a slot machine animation where the reels spin. to do this, I'm using drawRect to draw images in a custom class that inherits from UIView. I'm using an nstimer to update the position of the images and calling [self setNeedsDisplay] to update the drawing. In the simulator, it looks very good, however, on the device, it is very laggy. I was wondering if i'm doing something wrong with my method of drawing or is there any better solutions.
- (void)drawRect:(CGRect)rect
{
[image1 drawInRect:CGRectMake(0, image1Position, 98, 80)];
[image2 drawInRect:CGRectMake(0, image2Position, 98, 80)];
[image3 drawInRect:CGRectMake(0, image3Position, 98, 80)];
}
- (void)spin
{
// move each position down by 10 px
image1Position -= MOVEMENT;
image2Position -= MOVEMENT;
image3Position -= MOVEMENT;
// if any of the position <= -60 reset to 180
if(image1Position < -50)
{
image1Position = 180;
}
if(image2Position < -50)
{
image2Position = 180;
}
if(image3Position < -50)
{
image3Position = 180;
}
[self setNeedsDisplay];
}
-(void)beginSpinAnimation
{
timer = [NSTimer scheduledTimerWithTimeInterval:SCROLL_TIME target:self selector:#selector(spin) userInfo:self repeats:YES];
}
My CoreAnimation Attempt with UIScrollView:
- (void) spinToNextReel
{
int y = self.contentOffset.y + 80;
// if the current >= last element reset to first position (-30)
if(y >= (80 *(elementCount+1) - 30))
{
y = -30;
}
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDelegate:self];
[UIView setAnimationDuration:SCROLL_TIME];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
self.contentOffset = CGPointMake(0, y);
[UIView commitAnimations];
if (!isSpinning && targetY == y)
{
NSLog(#"target is %d, y is %d", targetY, y);
if(timer)
{
[timer invalidate];
timer = nil;
}
[self playSound];
}
}
I would say research into CoreAnimation. It is made to do what you want to do here. It'll be much faster than what you are doing here.
As for it being slow, calling drawInRect isn't the fastest thing in the world. What is SCROLL_TIME?
You want to use CoreAnimation, it will be a lot easier, and more efficient. Having said that, if you insist on trying to manually animate this way you are doing a couple of thing wrong:
Do not attempt move a constant amount on fixed intervals, timer events can be delayed, and if they are that will result in your animation being uneven, since you are moving a constant amount per event, not per time interval. You should record the actual timestamp every time you animate, compare it to the previous timestamp, and move an appropriate number of pixels. This will result in even amounts of movement even if the events are delayed (effectively you will being dropping frames).
Do not use an NSTimer, use a CADisplayLink. That will tie your drawing to the native refresh rate of the system, and synchronize it with any other drawing that is going on.
I know I said it already, but learn and use CoreAnimation, you will be much happier.