Rotating a UIImage to point in the right direction defined by calculated angle - objective-c

I have a cartoon character in a UIImage that is added as a subview to my main UIView. I take the centre coordinates of the image along with a randomly generated CGPoint and use trig to find the angle that the character needs to rotate to point in the right direction. I use UIView animation within a block. The first part does the rotate, then the completion for the first part does the move to the new location, then the completion for the inner block calls the animation loop again. This rotate/move sequence continues until the user stops it - so essentially, the character is continually moving around the screen.
It seems to be working for the first rotate/move sequence, but then it starts to face the wrong way. I've looked at the angles and they seem to be calculating correctly (I get that negative goes counter clockwise), so I'm wondering if there's something in how the translate to the new location and its associated new coordinates work? I've tried doing this with blocks, without blocks, changing to layers and using CGAffine functions, but this should be quite simple and easy to do with UIView animations.
In short, I'm stumped and need some help or pointers in the right direction. I'm probably missing something obvious so forgive me if that's the case but I've been at this for two weeks now so I'm a bit 'can't see the woods for the trees'!!
The pertinent code: I add the character (theBug), which is a custom class (that subclasses the UIImageView):
if (!doesContain){
NSLog(#"adding bug for first time");
//doesContain=YES; //set this at the end of the method after first call to animateLoop
[self.theBug setTranslatesAutoresizingMaskIntoConstraints:YES];
self.theBug=[[BugView alloc] initWithImage:[UIImage imageNamed:#"newbug1.png"]];
self.theBug.tag=3; //to identiry this in clearBoard
self.theBug.frame=CGRectMake(100, 100, 30, 30);
[self.gameView addSubview:self.theBug];
self.gameView.autoresizingMask=UIViewAutoresizingNone;
self.theBug.autoresizingMask=UIViewAutoresizingNone;
}
The I do the animation:
//Random x&y points
CGFloat x=(CGFloat) (arc4random() % (int) self.gameView.frame.size.width);
CGFloat y = (CGFloat) (arc4random() % (int) self.gameView.frame.size.height);
//create the random point to move the bug to
CGPoint rotateToLocation= CGPointMake(x, y);
//Store the bug's current location - This might not be the best way to represent this after rotation
CGPoint bugLocation = CGPointMake(self.theBug.frame.origin.x, self.theBug.frame.origin.y);
//Calculate the angle to rotate
float angleToRotate = atan2f(self.theBug.transform.tx-rotateToLocation.x, self.theBug.transform.ty - rotateToLocation.y);
if (self.theBug.transform.tx <x)
angleToRotate*=-1;
double delayInSeconds = 1;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[UIView animateWithDuration:4
delay:0
options:UIViewAnimationOptionCurveLinear
animations:^{
self.theBug.transform = CGAffineTransformRotate(self.theBug.transform, angleToRotate);
} completion:^(BOOL finished) {
[UIView animateWithDuration:4
delay:0.0
options:UIViewAnimationOptionCurveLinear
animations:^{
self.theBug.center = rotateToLocation;
} completion:^(BOOL finished) {
[self animationLoop:#"bug" finished:[NSNumber numberWithInt:0] context:nil];
}];
}];
});

The answer was simple! My calculations were correct, the problem was my starting image was oriented incorrectly. I rotated the original image to the right position and it all worked fine.

Related

Rotate UI Elements programmatically after shouldAutorotate

I have a viewController that should not "autorotate", but manually rotate specific GUI elements. The reason is that I use the front camera for taking a picture and I don't want the UIView that contains my UIImageView to be rotated.
My code looks like this:
-(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
[self performSelector:#selector(refreshView) withObject:nil afterDelay:1.0];
return NO; // don't autorotate!
}
and:
- (void) refreshView {
UIDeviceOrientation actualDeviceOrientation = [[UIDevice currentDevice] orientation];
float rotation = 0; // UIDeviceOrientationPortrait
if (actualDeviceOrientation == UIDeviceOrientationPortraitUpsideDown) rotation = 180;
else if (actualDeviceOrientation == UIDeviceOrientationLandscapeLeft) rotation = 90;
else if (actualDeviceOrientation == UIDeviceOrientationLandscapeRight) rotation = 270;
float rotationRadians = rotation * M_PI / 180;
[UIView animateWithDuration:0.4
animations:^(void) {
self.labelPrize.center = self.prizeView.center;
self.prizeView.transform = CGAffineTransformMakeRotation(rotationRadians);
} completion:^(BOOL finished){ }];
}
"labelPrize" is the label with the caption "20 EURO" that is seen on the screenshots below, "prizeView" is it's container. prizeView is the only GUI element that has constraints defined, which look like this:
Just for clarification, here's what "labelPrize" looks like:
And finally, here's what the app produces:
This is not what I want to achieve, I'd like "prizeView"/"labelPrize" to be
always aligned to the horizon
always in the exact center of the screen
Also worth mentioning: I'd like to add labels above (header) and a button below ("okay") my "labelPrize" and rotate/position them as well in refreshView().
Thanks for any help!
There are two big problems here. Let's take them one at a time.
(1)
self.labelPrize.center = self.prizeView.center;
Think about it. labelPrize is a subview of prizeView. So you are mixing apples with oranges as far as coordinate systems go: labelPrize.center is measured with respect to prizeView.bounds, but prizeView.center is measured with respect to self.view.bounds. To keep the center of labelPrize at the center of prizeView, position it at the midpoint of prizeView's bounds. (However, you should not have to move it at all because the transform transforms the bounds.)
(2)
Rotation view transforms and auto layout are deadly enemies, as I explain here. That is why rotating the transform of prizeView seems to shift its position as well. My answer there gives you several possible workarounds.

create animated circle and rectangle in uiview

i'd have to create some animated shapes in a uiview in my iphone app (with xcode), like these ones (circle , rectangle)
The first one should be a circle increasing and decreasing its radius, from 0 to x and viceversa; the second one is a rectangle increasing its length from 0 to x, and viceversa. The shapes should be animated infinitely... until the user taps some button to stop the animations at some point. How can i achieve those particular draws and animations? What's the best way to achieve that? And how to stop them?
Many thanks for any advice.
In my case I had to animate the movement of subviews of my UIView object and I accomplished it like this: break up the continuous animation into logical parts, animate a part and configure this animation to start another animation after it is done animating. Below you see my code as is (what it does is move subviews that are placed on a circle along that circle over a given angle).
- (void) animatePart: (NSString*) animationId finished: (NSNumber*) finished context: (void *) context
{
// the context contains an NSNumber which is the angle remaining to turn the pieces
CGFloat displacement = [trackSet currentDisplacementOnTrack:drag.trackIndex];
NSMutableDictionary* contextDict = (NSMutableDictionary*) context;
Move * move = (Move *) [contextDict objectForKey:ANIMCTX_MOVE];
//NSNumber *direction = (NSNumber *) [contextDict objectForKey:ANIMCTX_DIRECTION];
printf("> EVC animatePart: displ: %3.3f ", displacement);
if (FABS(displacement) < 0.1) {
printf(", ending animation\n");
// this is the last piece of animation
[trackSet completeMoveOnTrack:move.trackRef];
[self updatePiecesAnimatedFromModel];
self.animationContext = nil;
}
else {
CGFloat dir = displacement == 0 ? 0 : displacement / FABS(displacement);
CGFloat delta = -ANIM_DELTASTATION * dir;
printf(" EVC animatePart: delta: %3.3f\n", delta);
CGFloat newDelta = FABS(delta) < FABS(displacement) ? delta : -displacement;
[trackSet registerMoveOnTrack:drag.trackIndex by:newDelta];
[UIView beginAnimations:animationId context:context];
[UIView setAnimationDuration:0.1];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(animatePart:finished:context:)];
[self updatePiecesFromTrack:drag.trackIndex];
[UIView commitAnimations];
}
printf("< EVC animatePart\n");
}
As you can see, the UIView methods setDelegate: and animationDidStopSelector: are used to configure this animation to call a certain method after it's done animating. The selector passed here is that of this same method. This sounds like a recursive method (it even has a stop condition, just like recursive methods normally do), but it's not. So no worries about stack overflows ;-)
For completeness, to kickstart this animation I use the method below (copied verbatim from my code).
- (void) animateMove: (Move *) move direction: (int) direction
{
printf("> EVC animateMove: %d\n", direction * move.stations);
NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithObject:move forKey:ANIMCTX_MOVE];
[dict setObject:[NSNumber numberWithInt: direction] forKey:ANIMCTX_DIRECTION];
self.animationContext = dict;
//[trackSet openMoveOnTrack:move.trackRef];
// the move has already been carried out in the model, but is not yet reflected in the UI
// we don't need to call trackSet's openMoveOnTrack and closeMoveOnTrack
// for the animation we initially set the track's current displacement to that of the move
// then in a series of animated steps we decrement that displacement gradually to zero,
// while showing the intermediate results in the UI.
CGFloat moveDisplacement = (CGFloat) (move.stations);
[trackSet registerMoveOnTrack:move.trackRef by:moveDisplacement];
[self animatePart:#"displacementAnimation" finished:[NSNumber numberWithInt:0] context:dict];
printf("< EVC animateMove: \n");
}

UIView animateWithDuration not running second time

I've tried a lot of different options, and looked through about 15 stack answers and I just can not figure this out.
The code is basically trying to fade out, and then pop back, a view every time a tap happens. It works fine the first time, but will not work any subsequent times.
- (void)handleTap:(UIGestureRecognizer*)gestureRecognizer
{
self.view.transform = CGAffineTransformIdentity;
__block HelpScreenController* weakSelf = self;
[UIView animateWithDuration:10
delay:0
options:UIViewAnimationOptionCurveEaseOut
animations:^(void) {
weakSelf.view.alpha = 0;
}
completion:^(BOOL finished) {
if (finished) {
weakSelf.view.alpha = 100.0f;
[weakSelf.view.layer removeAllAnimations];
[weakSelf.view setNeedsDisplay];
}
}];
}
It runs perfectly the first tap - it smoothly transitions from opaque to fully transparent over a 10 second period. Second+ tap(s) it wil sit there for 10 seconds, then go transparent for a heart beat then go back to fully opaque again.
How can I get it to animate smoothly every time?
Thanks in advance!
alpha values are between 0.0f and 1.0f. Setting the alpha value in the completion block to 1.0f instead of 100.0f should fix the problem.
Because values larger than 1.0f are all completely opaque, you will not see the transition from 100.0f to 1.0f (99% of your animation), so the effective duration of the transition from 1.0f to 0.0f would just be about 0.1 seconds instead of 10 (not exactly, because of the animation curve, but you get the idea).
You could just use a CABasicAnimation instead. Try out something like this:
- (void)handleTap:(UIGestureRecognizer*)gestureRecognizer
{
CALayer *viewLayer = self.view.layer;
[viewLayer removeAllAnimations];
CABasicAnimation *fader = [CABasicAnimation animationWithKeyPath:#"opacity"];
fader.fromValue = [NSNumber numberWithFloat:0.0];
fader.toValue = [NSNumber numberWithFloat:1.0];
fader.duration = 10;//change the duration and autoreverses option to fit with your look
fader.autoreverses = YES;
fader.repeatCount = 0;
[viewLayer addAnimation:fader forKey:#"fadeAnimation"];
}
Hope it helps!

capturing touches, storing in an array and then animating the array contents

I am new to Objective-C but I have made some progress with my app through the vast amount of information online. My objective is to press a record button, then move an icon representing a ball across a view. I will capture the touches in an array then replay the movement of the ball by animating the touches by stepping through the coordinates in the array on clicking a play button.
The code I am testing is attempting to step through the array and animate each set of co-ordinates in turn. Only the last animation happens. I guess my whole approach might be incorrect. Please offer me some help and thanks for your time.
NSMutableArray *yourCGPointsArray = [[NSMutableArray alloc] init];
[yourCGPointsArray addObject:[NSValue valueWithCGPoint:CGPointMake(300, 001)]];
[yourCGPointsArray addObject:[NSValue valueWithCGPoint:CGPointMake(300, 300)]];
[yourCGPointsArray addObject:[NSValue valueWithCGPoint:CGPointMake(001, 300)]];
int i;
i=0;
while (i < 3) {
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:1];
[UIView setAnimationDelay:1.0];
CGPoint point = [[yourCGPointsArray objectAtIndex:i] CGPointValue];
player3.center = CGPointMake(point.x , point.y);
[UIView commitAnimations];
NSLog (#"i array %d", i);
NSLog (#"cgpoint x %f", point.x);
NSLog (#"cgpoint y %f", point.y);
i = i + 1;
}
}
Yes, your approach is incorrect. Right now, your code fires off three animations at essentially the same time -- the while loop won't wait for one animation to complete before starting the next. Instead, you should start one animation, wait for it to complete, start the next, wait for it to complete, and so on. You can do this by using a completion block with each animation to start the next animation. Take a look at +animateWithDuration:animations:completion:, for example. It allows you to specify the completion block for the animation.

How to rotate an object around a arbitrary point?

I want to rotate an UILabel around an arbitrary point in a circular manner, not a straight line. This is my code.The final point is perfect but it goes through a straight line between the initial and the end points.
- (void)rotateText:(UILabel *)label duration:(NSTimeInterval)duration degrees:(CGFloat)degrees {
/* Setup the animation */
[UILabel beginAnimations:nil context:NULL];
[UILabel setAnimationDuration:duration];
CGPoint rotationPoint = CGPointMake(160, 236);
CGPoint transportPoint = CGPointMake(rotationPoint.x - label.center.x, rotationPoint.y - label.center.y);
CGAffineTransform t1 = CGAffineTransformTranslate(label.transform, transportPoint.x, -transportPoint.y);
CGAffineTransform t2 = CGAffineTransformRotate(label.transform,DEGREES_TO_RADIANS(degrees));
CGAffineTransform t3 = CGAffineTransformTranslate(label.transform, -transportPoint.x, +transportPoint.y);
CGAffineTransform t4 = CGAffineTransformConcat(CGAffineTransformConcat(t1, t2), t3);
label.transform = t4;
/* Commit the changes */
[UILabel commitAnimations];
}
You should set your own anchorPoint
Its very much overkill to use a keyframe animation for what really is a change of the anchor point.
The anchor point is the point where all transforms are applied from, the default anchor point is the center. By moving the anchor point to (0,0) you can instead make the layer rotate from the bottom most corner. By setting the anchor point to something where x or y is outside the range 0.0 - 1.0 you can have the layer rotate around a point that lies outside of its bounds.
Please read the section about Layer Geometry and Transforms in the Core Animation Programming Guide for more information. It goes through this in detail with images to help you understand.
EDIT: One thing to remember
The frame of your layer (which is also the frame of your view) is calculated using the position, bounds and anchor point. Changing the anchorPoint will change where your view appears on screen. You can counter this by re-setting the frame after changing the anchor point (this will set the position for you). Otherwise you can set the position to the point you are rotating to yourself. The documentation (linked to above) also mentions this.
Applied to you code
The point you called "transportPoint" should be updated to calculate the difference between the rotation point and the lower left corner of the label divided by the width and height.
// Pseudocode for the correct anchor point
transportPoint = ( (rotationX - labelMinX)/labelWidth,
(rotationX - labelMinY)/labelHeight )
I also made the rotation point an argument to your method. The full updated code is below:
#define DEGREES_TO_RADIANS(angle) (angle/180.0*M_PI)
- (void)rotateText:(UILabel *)label
aroundPoint:(CGPoint)rotationPoint
duration:(NSTimeInterval)duration
degrees:(CGFloat)degrees {
/* Setup the animation */
[UILabel beginAnimations:nil context:NULL];
[UILabel setAnimationDuration:duration];
// The anchor point is expressed in the unit coordinate
// system ((0,0) to (1,1)) of the label. Therefore the
// x and y difference must be divided by the width and
// height of the label (divide x difference by width and
// y difference by height).
CGPoint transportPoint = CGPointMake((rotationPoint.x - CGRectGetMinX(label.frame))/CGRectGetWidth(label.bounds),
(rotationPoint.y - CGRectGetMinY(label.frame))/CGRectGetHeight(label.bounds));
[label.layer setAnchorPoint:transportPoint];
[label.layer setPosition:rotationPoint]; // change the position here to keep the frame
[label.layer setTransform:CATransform3DMakeRotation(DEGREES_TO_RADIANS(degrees), 0, 0, 1)];
/* Commit the changes */
[UILabel commitAnimations];
}
I decided to post my solution as an answer. It works fine accept it doesn't have the old solutions's curve animations (UIViewAnimationCurveEaseOut), but I can sort that out.
#define DEGREES_TO_RADIANS(angle) (angle / 180.0 * M_PI)
- (void)rotateText:(UILabel *)label duration:(NSTimeInterval)duration degrees:(CGFloat)degrees {
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddArc(path,nil, 160, 236, 100, DEGREES_TO_RADIANS(0), DEGREES_TO_RADIANS(degrees), YES);
CAKeyframeAnimation *theAnimation;
// animation object for the key path
theAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
theAnimation.path=path;
CGPathRelease(path);
// set the animation properties
theAnimation.duration=duration;
theAnimation.removedOnCompletion = NO;
theAnimation.autoreverses = NO;
theAnimation.rotationMode = kCAAnimationRotateAutoReverse;
theAnimation.fillMode = kCAFillModeForwards;
[label.layer addAnimation:theAnimation forKey:#"position"];
}
CAKeyframeAnimation is the right tool for this job. Most UIKit animations are between start and end points. The middle points are not considered. CAKeyframeAnimation allows you to define those middle points to provide a non-linear animation. You will have to provide the appropriate bezier path for your animation. You should look at this example and the one's provided in the Apple documentation to see how it works.
translate, rotate around center, translate back.