Rotating NSView around center - objective-c

I'm trying to animate the rotation of an NSView around its center, but it keeps shifting side to side during rotation. What is causing this?
-(void)startRefreshAnimation {
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:1.0];
[[NSAnimationContext currentContext] setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];
[NSAnimationContext currentContext].completionHandler = ^{ [self startRefreshAnimation]; };
[[view animator] setFrameCenterRotation:previousRotation - 90.0];
previousRotation += -90.0;
[NSAnimationContext endGrouping];
}
Shift up during rotation:
Shift down during rotation:

here is the dummy project and the answer that I found. (For Cocoa Applicaiton)
Rotate NSImageView
github project (download)

From the documentation:
If the application has altered the layer’s anchorPoint property, the behavior is undefined. Sending this message to a view that is not managing a Core Animation layer causes an exception.
https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ApplicationKit/Classes/NSView_Class/Reference/NSView.html
Is your view managing a CALayer with an unmodified anchor point?
EDIT
I setup similar code at got the exact same results. No adjust of origin or anchor point could resolve this problem. My theory is that this particular method contains bugs (does for autolayout), or works in a way which we don't anticipate. I achieved the correct effect using CABasicAnimation.
/* setup */
....
_view.layer.anchorPoint = CGPointMake(0.5f, 0.5f);
_view.layer.position = ...
[self startRefreshAnimation];
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
[self startRefreshAnimation];
}
-(void)startRefreshAnimation {
CABasicAnimation *anim2 = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
anim2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
anim2.fromValue = [NSNumber numberWithFloat:previousRotation * (M_PI / 180.0f)];
anim2.toValue = [NSNumber numberWithFloat:(previousRotation + 90.0f) * (M_PI / 180.0f)];
previousRotation = previousRotation + 90.0f;
anim2.duration = 1.0f;
anim2.delegate = self;
[_view.layer addAnimation:anim forKey:#"transform"];
}

Related

how to animate both the frame of an UIView and the frame of one of its sublayers?

What I have:
I have a UIView (named pView) which has as sublayer a CAGradientLayer. Practically is this:
ViewController -> View ->pView -> CAGradientLayer
This is the code that creates all this:
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UITestView * pView = nil;
UIColor * scolor = nil, *ecolor = nil;
self.view.backgroundColor = [UIColor yellowColor];
pView = [[UITestView alloc] initWithFrame: CGRectMake(self.view.frame.origin.x, 100.0, self.view.frame.size.width, 100.0)];
scolor = [UIColor colorWithRed:(14/255.0) green: (238/255.0) blue:(123/255.0) alpha:1];
ecolor = [UIColor colorWithRed:(6/255.0) green: (216/255.0) blue:(69/255.0) alpha:1];
// creating the gradient layer
CAGradientLayer * layer = [[CAGradientLayer alloc] init];
layer.frame = self.bounds;
layer.colors = #[(id)scolor.CGColor,(id)ecolor.CGColor];
[pView.layer insertSublayer:layer atIndex:0];
// creating a tapGestureRecognizer
[pView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapInViews:)]];
[self.view addSubview:pView];
touched = NO;
}
....
#end
What I'm trying to do
Once a tap gesture is detected over pView I want to increase the height of pView by 100.0 but animatedly. If pView was previously touched (that is, its height was already increased by 100.0) I want to decrease the height of pView by 100.0 (that is, returning it to its original size);
What I already know
I know that since I want to change the frame of pView I must change also the frame (or bounds) of the CAGradientLayer attached to pView. Since I want to animate these changes, I want these animations occurs at the same time and have the same duration.
I know that the frame (or bounds) of a layer is only animatable inside an animation block.
What I do:
This is the option I've test:
- (void) handleTapInViews: (nonnull UITapGestureRecognizer *) sender {
pView = sender.view;
if (touched) {
[UIView animateWithDuration:0.4 animations:^{
pView.frame = CGRectMake(pView.frame.origin.x, pView.frame.origin.y, pView.frame.size.width, pView.frame.size.height - 100.0);
pView.layer.sublayers[0].frame = pView.bounds;
[self.view setNeedsLayout];
} completion:^(BOOL finished) {
touched = NO;
}];
}
else {
[UIView animateWithDuration:0.4 animations:^{
pView.frame = CGRectMake(pView.frame.origin.x, pView.frame.origin.y, pView.frame.size.width, pView.frame.size.height + 100.0);
pView.layer.sublayers[0].frame = pView.bounds;
[self.view setNeedsLayout];
} completion:^(BOOL finished) {
touched = YES;
}];
}
}
This option actually works (that is, both the frame of pView and the frame of the layer change animatedly) but they are not synchronised; that is, the changes in the frame of the layer are slightly (but perceptible) more faster than the changes in the frame of pView. This effect is more evident when the height of pView is decreased.
I 've also read about CABasicAnimation and CAAnimationGroup, in order to animate both the bounds and position of the layer. In this case also the animation of the layer is bit faster than the animation of the view (it is perceptible and it is not a matter of seconds in case anyone ask about setting duration or whatever) and also in this case, after the animation the bound of the layer return to its original size which is not the desired effect. I already know this last matter can be fixed assigned the new values to the layer at the end of the animation but I certainty do not know where in my code put that.
Most of what i've read regarding this other option is from these links:
link1
link2
link3
link4
In any case, does anybody please knows how can I fix this?? thanks in advance.
Well I ended setting the backgroundColor of Pview as ecolor = [UIColor colorWithRed:(6/255.0) green: (216/255.0) blue:(69/255.0) alpha:1. This way, the effect I commented about the animation of the layer been faster than the animation of the view is not noticeable now. I think maybe this is not the proper answer but it suit me.

Why does this code not properly center scene when doing UIPanGestureRecognizer?

I am a beginner developing a program for the AppStore using Xcode's sprite kit. I made a 'test' program so I can try out new things before adding it to my game. Right now I am fiddling around with swiping a scene - I have an SKNode (called "background") where I am adding several children as SKSpriteNodes. One sprite node is visible on the initial scene (the center, position 160,240), and two more that are not visible: to the left of the scene (position -160,240), and to the right of the scene (position 480,240).
I would like my game to be able to swipe left or right, and when it swipes left or right, the view will auto-center itself (with animation) to one of the three SKSpriteNodes. My code using the UIPanGestureRecognizer to move the background node works properly, and my code for auto-centering the view works MOSTLY (background position set to 0,0 or -320,0 or +320,0), but sometimes it has a strange offset and doesn't completely center itself (for example, the background position will be 7,0 or -34,0 when I pan right or left). What am I doing wrong?
P.S: I am using code from RayWenderlich's "iOS Games" for the SKTMoveEffect. I also want to note that if I make the function f(t)=t, there is no problem (at least in my several tests), but f(t)=t^2 or anything else seems to have an issue; if it helps to see the code for this I can post it too
#implementation LTMyScene
{
SKNode *background;
SKSpriteNode *spaceship1, *spaceship2;
}
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
/* Setup your scene here */
background=[SKNode node];
[self addChild:background];
self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0];
SKLabelNode *myLabel = [SKLabelNode labelNodeWithFontNamed:#"Chalkduster"];
myLabel.text = #"Hello, World!";
myLabel.fontSize = 30;
myLabel.position = CGPointMake(CGRectGetMidX(self.frame),
CGRectGetMidY(self.frame));
[background addChild:myLabel];
spaceship1=[SKSpriteNode spriteNodeWithImageNamed:#"Spaceship.png"];
spaceship1.position=CGPointMake(-self.size.width/2, self.size.height/2);
spaceship1.anchorPoint=CGPointMake(0.5, 0.5);
[background addChild:spaceship1];
spaceship2=[SKSpriteNode spriteNodeWithImageNamed:#"Spaceship.png"];
spaceship2.position=CGPointMake(self.size.width*3/2, self.size.height/2);
spaceship2.anchorPoint=CGPointMake(0.5, 0.5);
[background addChild:spaceship2];
}
return self;
}
- (void)didMoveToView:(SKView *)view
{
UIPanGestureRecognizer *swipe = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(dragPlayer:)];
[[self view] addGestureRecognizer:swipe];
}
-(void)dragPlayer: (UIPanGestureRecognizer *)gesture {
[[[self view] layer] removeAllAnimations];
CGPoint trans = [gesture translationInView:self.view];
SKAction *moveAction = [SKAction moveByX:trans.x y:0 duration:0];
[background runAction:moveAction];
[gesture setTranslation:CGPointMake(0, 0) inView:self.view];
if([gesture state] == UIGestureRecognizerStateEnded) {
CGFloat finalX=0;
if (abs(background.position.x)<self.size.width/2) {
finalX=0;
} else if (abs(background.position.x)<self.size.width*3/2) {
finalX=self.size.width*background.position.x/abs(background.position.x);
}
NSLog(#"%f",finalX);
SKTMoveEffect *upEffect =
[SKTMoveEffect effectWithNode:background duration:0.5
startPosition:background.position
endPosition:CGPointMake(finalX, 0)];
upEffect.timingFunction = ^(float t) {
// return powf(2.0f, -3.0f * t) * fabsf(cosf(t * M_PI * 1.0f)) //has bounce
// return (-1.0f*t*t+1) //no bounce ... for parabola this is only solution with (1,0) and (0,1) as intercepts and vertex at (1,0)
return (t*t)
;};
SKAction *upAction = [SKAction actionWithEffect:upEffect];
[background runAction:upAction];
}
}
-(void)update:(CFTimeInterval)currentTime {
/* Called before each frame is rendered */
NSLog(#"%f,%f",background.position.x,background.position.y);
}
#end
You have to remember that you are moving the larger background node with other nodes as its children. Keeping that in mind, the code you need to center on a specific node is:
_worldNode.position = CGPointMake(-(myNode.position.x-(self.size.width/2)), -(myNode.position.y-(self.size.height/2)));
The above assumes that your main background "canvas" is called _worldNode and your target node is a child of _worldNode

CABasicAnimation move frame left by 300px

I need help with CABasicAnimation. I am trying to move a NSView left by 300 pixels. I found this SO thread: How to animate the frame of an layer with CABasicAnimation?
Turns out animating the frame is not possible and one of the answer points to a link to QA on Apple's website but it takes me a to a generic page:
http://developer.apple.com/library/mac/#qa/qa1620/_index.html
So, how can I do something as simple as translation of my NSView/CALyer?
Thanks!
NSView has a protocol called NSAnimatablePropertyContainer which allows you to create basic animations for views:
The NSAnimatablePropertyContainer protocol defines a way to add
animation to an existing class with a minimum of API impact ...
Sending of key-value-coding compliant "set" messages to the proxy will
trigger animation for automatically animated properties of its target
object.
The NSAnimatablePropertyContainer protocol can be found here
I recently used this technique to change the origin of a frame:
-(void)setOrigin:(NSPoint)aPoint {
[[self animator] setFrameOrigin:aPoint];
}
Instead of calling the [view setFrameOrigin:], I created another method called setOrigin: which then applies the setFrameOrigin: call to the view's animator.
If you need to change the duration of the animation, you can do so like this (similar to CATransactions):
-(void)setOrigin:(NSPoint)aPoint {
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setCompletionHandler:^{
...Completion Callback Code goes here...
}];
[[NSAnimationContext currentContext] setDuration:1.0];
[[self animator] setFrameOrigin:aPoint];
[NSAnimationContext endGrouping];
}
The NSAnimationContext is described here
You can animate center property instead. Eg:
//assuming view is your NSView
CGPoint newCenter = CGPointMake(view.center.x - 300, view.center.y);
CABasicAnimation *animation = [CABasicAnimation animation];
//setup your animation eg. duration/other options
animation.fromValue = [NSValue valueWithCGPoint:v.center];
animation.toValue = [NSValue valueWithCGPoint:newCenter];
[view.layer addAnimation:animation forKey:#"key"];

iOS/Objective-C: transitionWithView custom animation block resizes view immediately, not over duration

The following code is a method I created inside a UIViewController to popup/down a "reader" overlay on top of the controller's own view. The intention is for the reader to begin as transparent, size zero, at a specific point. "Popup" is then animated as increasing in opacity and size, and shifts towards an application frame central position. "Popdown" is subsequently animated as the reverse, shrinking back whilst moving toward a specified location, fading out.
The popup code works exactly as desired. However, the popdown version (i.e. code executed if isPopup == NO) immediately changes the bounds rather than doing so gradually. Thus the popdown animation shows from the beginning a 1 pixel square view moving towards its destination and fading out.
-(void)popupReader:(BOOL)isPopup from:(CGPoint)loc {
CGFloat newAlpha = 0.0f;
CGPoint newCenter = CGPointZero;
CGRect newBounds = CGRectZero;
CGRect appFrame = [UIScreen mainScreen].applicationFrame;
CGSize readerSize = [self viewSize];
if (isPopup) {
newAlpha = 1.0f;
newCenter = CGPointMake(appFrame.origin.x+appFrame.size.width/2,
appFrame.origin.y+appFrame.size.height/2);
newBounds = CGRectMake(0,0,readerSize.width,readerSize.height);
[self.view setAlpha:0.0f];
[self.view setCenter:loc];
[self.view setBounds:CGRectMake(0, 0, 0, 0)];
} else {
newCenter = loc;
newBounds = CGRectMake(0,0,1,1);
}
const CGFloat animDur = 0.3f;
[UIView transitionWithView:self.view
duration:animDur
options:UIViewAnimationOptionTransitionNone|UIViewAnimationOptionCurveEaseOut
animations:^{
self.view.alpha = newAlpha;
self.view.center = newCenter;
self.view.bounds = newBounds;
}
completion:nil];
}
I've already tried animating just the frame, rather than bounds and center, but the result was identical.
Does anyone know why this is happening, and how I can overcome this problem?
Many thanks for your time.
from the docs:
UIViewAnimationOptionTransitionNone -
An option for specifying that no transition should occur.
try one of:
UIViewAnimationOptionTransitionFlipFromLeft
UIViewAnimationOptionTransitionFlipFromRight
UIViewAnimationOptionTransitionCurlUp
UIViewAnimationOptionTransitionCurlDown
Edit:
Or if you're just looking to animate those values, try
[UIView animateWithDuration:(NSTimeInterval)duration
animations:(void (^)(void))animations
completion:(void (^)(BOOL finished))completion];

Animating Views with Core Animation Layer

I have a NSWindow containing a NSView with 'Wants Core Animation Layer' enabled. The view then contains many NSImageView that use are initially animated into position. When I run the animation, it is extremely sluggish and drops most of the frames. However, if I disable 'Wants Core Animation Layer' the animation works perfectly. I'm going to need the core animation layer but can't figure out how to get it to perform adequately.
Can I do anything to fix the performance issues?
Here is the code:
// AppDelegate
NSRect origin = ...;
NSTimeInterval d = 0.0;
for (id view in views)
{
[view performSelector:#selector(animateFrom:) withObject:origin afterDelay:d];
d += 0.05f;
}
// NSImageView+Animations
- (void)animateFrom:(NSRect)origin
{
NSRect original = self.frame;
[self setFrame:origin];
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:0.20f];
[[self animator] setFrame:original];
[NSAnimationContext endGrouping];
}
It's possible that the NSTimer is killing your performance. Core Animation has rich support for controlling the timing of animations through the CAMediaTiming protocol, and you should take advantage of that in your app. Instead of using the animator proxy and NSAnimationContext, try using Core Animation directly. If you create a CABasicAnimation for each image and set its beginTime, it will delay the start of the animation. Also, for the delay to work the way you want, you must wrap each animation in a CAAnimationGroup with its duration set to the total time of the entire animation.
Using the frame property could also be contributing to the slowdown. I really like to take advantage of the transform property on CALayer in situations like this where you're doing an "opening" animation. You can lay out your images in IB (or in code) at their final positions, and right before the window becomes visible, modify their transforms to the animation's starting position. Then, you just reset all of the transforms to CATransform3DIdentity to get the interface into its normal state.
I have an example in my <plug type="shameless"> upcoming Core Animation book </plug> that's very similar to what you're trying to do. It animates 30 NSImageViews simultaneously with no dropped frames. I modified the example for you and put it up on github. These are the most relevant bits of code with the extraneous UI stuff stripped out:
Transform the layers to their start position
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// ... SNIP ... //
//Start with all of the images at the origin
[CATransaction begin];
[CATransaction setDisableActions:YES];
for (CALayer *imageLayer in [[[self imageContainer] layer] sublayers]) {
CGPoint layerPosition = [layer position];
CATransform3D originTransform = CATransform3DMakeTranslation(20.f - layerPosition.x, -layerPosition.y, 0.f);
[imageLayer setTransform:originTransform];
}
[CATransaction commit];
}
Animate the transform back to the identity
- (IBAction)runAnimation:(id)sender {
CALayer *containerLayer = [[self imageContainer] layer];
NSTimeInterval delay = 0.f;
NSTimeInterval delayStep = .05f;
NSTimeInterval singleDuration = [[self durationStepper] doubleValue];
NSTimeInterval fullDuration = singleDuration + (delayStep * [[containerLayer sublayers] count]);
for (CALayer *imageLayer in [containerLayer sublayers]) {
CATransform3D currentTransform = [[imageLayer presentationLayer] transform];
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform"];
anim.beginTime = delay;
anim.fromValue = [NSValue valueWithCATransform3D:currentTransform];
anim.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
anim.fillMode = kCAFillModeBackwards;
anim.duration = singleDuration;
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = [NSArray arrayWithObject:anim];
group.duration = fullDuration;
[imageLayer setTransform:CATransform3DIdentity];
[imageLayer addAnimation:group forKey:#"transform"];
delay += delayStep;
}
}
I also have a video on YouTube of the example in action if you want to check it out.
Did you try to batch everything in a CATransaction?
[CATransaction begin];
for {...}
[CATransaction commit];
CATransaction is the Core Animation mechanism for batching multiple layer-tree operations into atomic updates to the render tree.