Animating label (NSTextField) horizontally - objective-c

I have a NSTextField as a label, showing a string.
I want to animate this label from right to left, if the content of it is too large to be displayed at once.
I've done this with an NSTimer so far, it works, but it's just not a very good solution.
The labels are displayed in an NSTextFieldCell, in a Table View.
They often get out of sync, and I guess it's just eating up a lot of CPU/GPU resources.
Is there another way with Core Animation to do this?
I have tried it with layers, as you can see right here:
CALayer and drawRect
but I didn't get it working either.
I would really appreciate your help.

You can simply animate the position of NSTextField with animator like
[[textField animator] setFrameOrigin:NSMakePoint(x,y)];
you can also embed it in "CATrancation" code like this:
[CATransaction begin];
[CATransaction setAnimationDuration:0.5];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[[textField animator] setFrameOrigin:NSMakePoint(x,y)];
[CATransaction commit];
if you need animation delegate, you can use CABasicAnimation
CABasicAnimation* animation = [CABasicAnimation animation];
animation.delegate = self;
NSDictionary *animations = [NSDictionary dictionaryWithObjectsAndKeys:animation,#"frameOrigin",nil];
[textField setAnimations:animations];
[[textField animator] setFrameOrigin:NSMakePoint(x,y)];
Delegate methods are
- (void)animationDidStart:(CAAnimation *)theAnimation;
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag;
If you need to mask your text field, just embed it in other NSView.

First, animate the label using one of the functions offered in the other answers.
Then, if you want to display another view on the sides without overlapping, you can:
Insert the label in a subview with the limits you wish
Use bringSubviewtoFront: or sendSubviewToBack: to make sure your label stays in the back

Related

CALayer and drawRect

I'm completely new to Core Animation, CALayer and all this stuff, so bear with me.
I have a custom NSTextField using as a Label. I would want the content to animate it's position, so the whole string get's visible if it's too long for the Labels width.
Now, the animation itself is working fine. I've implemented this with CABasicAnimation:
- (void)awakeFromNib {
CALayer *newLayer = [CALayer layer];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
[animation setFromValue:[NSValue valueWithPoint:NSMakePoint(0, 0)]];
[animation setToValue:[NSValue valueWithPoint:NSMakePoint(-self.attributedStringValue.size.width, 0)]];
[animation setDuration:5.0];
[animation setRepeatCount:HUGE_VAL];
[newLayer addAnimation:animation forKey:#"position"];
[self setLayer:newLayer];
[self setWantsLayer:YES];
}
The only problem is, that the drawRect: method only draws what's on the screen.
So I thought I would override the drawRect: method to draw the whole attributed string. But if I do this, nothing get's drawn at all...
Can anyone point me into the right direction?
Thank you!
In general, you want to avoid overriding drawRect if at all possible, especially for CALayer objects that you are animating. That tends to lead to really dreadful performance.
What do you mean "the drawRect: method only draws what's on the screen?"
It only draws the portion of the string that's currently visible?
I ended up using an NSTimer. Not the most beautiful solution, but at least it works.

How to synchronously animate a UIView and a CALayer

To illustrate this question, I gisted a very small Xcode project on Github (two classes, 11kb download). Have a look at the gist here or use git clone git#gist.github.com:93982af3b65d2151672e.git.
Please consider the following scenario. A custom view of mine, called 'container view', contains two little squares. Illustrated in this screenshot:
The blue square is a 22x22 pt UIView instance and the red square is a 22x22 pt CALayer instance. For purposes of this question I want the two squares to 'stick' to the bottom right corner of the outer view, while I animate the frame of that outer view.
I change the container view's frame within a UIView's class method animateWithDuration:delay:options:animations:completion:, with a non-default easing parameter of type UIViewAnimationCurveEaseOut.
[UIView animateWithDuration:1.0 delay:0
options:UIViewAnimationCurveEaseOut
animations:^{ _containerView.frame = randomFrame; }
completion:nil];
Note: Both the blue UIView's and the red CALayer's frame are set in the overridden setFrame: method of the container view, but results would have been the same if I had set the blue UIView's autoresizingMask property to UIView flexible left and top margins.
In the resulting animation, the blue square 'sticks' to the corner just the way I intended it, but the red square completely disregards both timing and easing of the animation. I assume this is because of the implicit animation feature of Core Animation, a feature that has helped me in many occasions before.
Here are a few screenshots in the animation sequence that illustrate the asynchronicity of the two squares:
On to the question: Is there a way to synchronize the two frame changes so that the red CALayer and the blue UIView both move with the same animation duration and easing curve, sticking to each other as if they were one view?
P.S. Of course the required visual result of the two squares sticking together could be achieved in any number of ways, for example by having both layers become either CALayers or UIViews, but the project that the real issue is in has a very legit cause for the one to be a CALayer and the other to be a UIView.
I'm assuming that you got the end position correct and that after the animation that the view and the layer are aligned. That has nothing to do with the animation, just geometry.
When you change the property of a CALayer that isn't the layer of a view (like in your case) it will implicitly animate to its new value. To customize this animation you could use an explicit animation, like a basic animation. Changing the frame in a basic animation would look something like this.
CABasicAnimation *myAnimation = [CABasicAnimation animationWithKeyPath:#"frame"];
[myAnimation setToValue:[NSValue valueWithCGRect:myNewFrame]];
[myAnimation setFromValue:[NSValue valueWithCGRect:[myLayer frame]]];
[myAnimation setDuration:1.0];
[myAnimation setTimingFunction:[CAMediaTimingFuntion functionWithName: kCAMediaTimingFunctionEaseOut]];
[myLayer setFrame:myNewFrame];
[myLayer addAnimation:myAnimation forKey:#"someKeyForMyAnimation"];
If the timing function and the duration of both animations are the same then they should stay aligned.
Also note that explicit animations doesn't change the value and hat you have to both add the animation and set the new value (like in the sample above)
Yes, there are a number of ways to achieve the same effect. One example is having the view and the layer be subviews of the same subview (that in turn is a subview of the outer frame).
Edit
You can't easily group the UIView-animation with an explicit animation. Neither can you use an animation group (since you are applying them to different layers) but yo can use a CATransaction:
[CATransaction begin];
[CATransaction setAnimationDuration:1.0];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFuntion functionWithName: kCAMediaTimingFunctionEaseOut]];
// Layer animation
CABasicAnimation *myAnimation = [CABasicAnimation animationWithKeyPath:#"frame"];
[myAnimation setToValue:[NSValue valueWithCGRect:myNewFrame]];
[myAnimation setFromValue:[NSValue valueWithCGRect:[myLayer frame]]];
[myLayer setFrame:myNewFrame];
[myLayer addAnimation:myAnimation forKey:#"someKeyForMyAnimation"];
// Outer animation
CABasicAnimation *outerAnimation = [CABasicAnimation animationWithKeyPath:#"frame"];
[outerAnimation setToValue:[NSValue valueWithCGRect:myNewOuterFrame]];
[outerAnimation setFromValue:[NSValue valueWithCGRect:[outerView frame]]];
[[outerView layer] setFrame:myNewOuterFrame];
[[outerView layer] addAnimation:outerAnimation forKey:#"someKeyForMyOuterAnimation"];
[CATransaction commit];
If you want David Rönnqvist's snippet in Swift 3.0, here you have it:
CATransaction.begin()
CATransaction.setAnimationDuration(1.0)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut))
// Layer animation
let myAnimation = CABasicAnimation(keyPath: "frame");
myAnimation.toValue = NSValue(cgRect: myNewFrame)
myAnimation.fromValue = NSValue(cgRect: myLayer.frame)
myLayer.frame = myNewFrame
myLayer.add(myAnimation, forKey: "someKeyForMyAnimation")
// Outer animation
let outerAnimation = CABasicAnimation(keyPath: "frame")
outerAnimation.toValue = NSValue(cgRect: myNewOuterFrame)
outerAnimation.fromValue = NSValue(cgRect: outerView.frame)
outerView.layer.frame = myNewOuterFrame
outerView.layer.add(outerAnimation, forKey: "someKeyForMyOuterAnimation")
CATransaction.commit()

ios transform.rotation.y breaks up window

I have an odd problem in one of my chain animations. I have block object for each of the blocks on my screen (26 of them), and when the user presses them I perform a flip. It works great, but I'm adding a scale animation before I do these to make them about 50% bigger on the screen. So, my sequence is:
enlarge
delay (let user see the large image)
spin-out 90% (removes block from view) - using transform.rotation.y
change image & spin-in
shrink.
I have setup the window view controller to be a delegate of these blocks, such that it can pass a counter to the blocks so I can position the right sublayer on the top (using setZposition).
It all works great, except when I have 2 blocks on positioned above/below each other on the screen, such that the enlarge will cause them to overlap, and then when the spin-out animation starts, it immediately has the right side of the block pop behind the block below it. I've tried changing the animation to transform.rotation.x and get the same behavior when the blocks are side-to-side.
I'm not sure if it's an iOS bug of if I'm just not doing something correct. Any suggestions are greatly appreciated. Here is the spin-out method:
- (void)spinOut:(id)sender
{
NSTimeInterval animationTime=0.85;
[UIView animateWithDuration:animationTime
delay:0
options: UIViewAnimationOptionCurveLinear
animations:^{
// setup the animation to spin the current view out
[CATransaction begin];
[CATransaction setDisableActions:YES];
CABasicAnimation *spinOut = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
[spinOut setDelegate:self];
[spinOut setDuration:animationTime];
CAMediaTimingFunction *tf = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[spinOut setTimingFunction:tf];
[spinOut setFromValue:[NSNumber numberWithFloat:M_PI * 0.0]];
[spinOut setToValue:[NSNumber numberWithFloat:M_PI * 0.5]];
[spinOut setRemovedOnCompletion:NO];
[spinOut setFillMode:kCAFillModeForwards];
// setup variables used to roll in the next view on animation completion.
[pageView.layer addAnimation:spinOut forKey:kMyVeryOwnABCsSpinOutKey];
[CATransaction commit];
}
completion:^(BOOL finished){
[self setFlipOutAnimationTimer:[NSTimer scheduledTimerWithTimeInterval:animationTime target:self selector:#selector(spinIn:) userInfo:nil repeats:NO]];
}];
}
OK - this is an old question, but I kindof figured out this problem. Since I was flipping these images around the Y axis, I was able to change the animated image's Zposition. In my startAnimation method, I added:
float pageLevelLayerPosition;
// logic to set the zposition variable 100 more than anything nearby
_originalImageViewZPosition = bigImageView.layer.zPosition;
[bigImageView.layer setZPosition:pageLevelLayerPosition];
and then when in the animationDidStop, I added the relevant line of code to reset it back to 0 where I was removing the animation. I had some code to manage the variable so that I was assured that the number was greater than anything nearby.
I guess if I ever get into 3-D animation, then I'll need to control this much more closely, but that is what was causing my problem.

CATransaction duration not working

I set a few CALayer transform and bounds modifications, within a CATransaction. However, regardless the method I use (key-value, setAnimationDuration) there is no animation, the changes are done, but immediately without transition.
Do you have any idea why?
Thanks!
/* CALayer*layer=[CALayer layer];
layer.bounds =AnUIImageView.bounds;
layer.contents=AnUIImageView.layer.contents;
[AnotherUIImageView.layer addSublayer:layer];
CGPoint thecentre=AnUIImageView.center;
CALayer* layerInTarget=[AnotherUIImageView.layer.sublayers lastObject];
[layerInTarget setPosition:[self.view convertPoint:thecentre toView:AnotherUIImageView]];
AnUIImageView.layer.hidden=YES;
*/ // the code above works, i show it to be complete
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:2.0f]
forKey:kCATransactionAnimationDuration];
layerInTarget.position=[self.view convertPoint:AnotherUIImageView.center toView:AnotherUIImageView];
layerInTarget.transform=CATransform3DMakeScale(0.6,0.6,0.6);
[CATransaction commit];
CALayers that are associated with a UIView (as in, they're accessed via view.layer) do not participate in implicit animations, regardless of how you configure your CATransaction. You either need to use explicit animations (using the appropriate subclass of CAAnimation) or you need to use UIView animations.

CALayer flickers when drawing a path

I am using a CALayer to display a path via drawLayer:inContext delegate method, which resides in the view controller of the view that the layer belongs to. Each time the user moves their finger on the screen the path is updated and the layer is redrawn. However, the drawing doesn't keep up with the touches: there is always a slight lag in displaying the last two points of the path. It also flickers, but only while displaying the last two-three points again. If I just do the drawing in the view's drawRect, it works fine and the drawing is definitely fast enough.
Does anyone know why it behaves like this? I suspect it is something to do with the layer buffering, but I couldn't find any documentation about it.
[UIView new] is simply shorthand for [[UIView alloc] init].
Give the following before setNeedsDisplay method::
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:someDelay] forKey:kCATransactionAnimationDuration];
[aLayer setNeedsDisplay];
[CATransaction commit];
You might have better luck using a layer hosting view.
Instead of using the drawLayer:inContext: method, setup the view you want and add a CALayer to it:
UIView *layerHosting = [[UIView alloc] initWithFrame:frame];
[layerHosting setLayer:[[CALayer new] autorelease]];
Hope that helps!