I am creating an app that involves animating lines within a workspace over time. My current approach for this is to use code like this in drawRect:
CGContextSetStrokeColor(context, black);
CGContextBeginPath(context);
CGContextMoveToPoint(context, startPoint.x, startPoint.y);
CGContextAddLineToPoint(context, finalPoint.x, finalPoint.y);
CGContextStrokePath(context);
...and then just setting a timer to run every 0.05 seconds to update finalPoint and call setNeedsDisplay.
I'm finding this approach (when there's 5ish lines moving at once) slows down the app terribly, and even with such a high refresh frequency, still appears jerky.
There must be some better way to perform this very simple line drawing in an animated line - i.e. saying that I want a line to start at x1, y1 and stretching to x2, y2 over a given length of time. What are my options for this? I need to make this perform faster and would love to get rid of this clunky timer.
Thanks!
Use CAShapeLayers, and a combination of CATransactions and CABasicAnimation.
You can add a given path to a shapeLayer and let it do the rendering.
A CAShapeLayer object has two properties called strokeStart and strokeEnd, which defines where along the path the end of the line should render. The defaults are 0.0 for strokeStart, and 1.0 for strokeEnd.
If you set up your path so that strokeEnd initially starts at 0.0, you will see no line.
You can then animate, from 0.0 to 1.0, the strokeEnd property and you will see the line lengthen.
To change CAShapeLayer's implicit 0.25s default animation timing, you can add a function to the class, like so:
-(void)animateStrokeEnd:(CGFloat)_strokeEnd {
[CATransaction begin];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:keyPath];
animation.duration = 2.0f; //or however long you want it to happen
animation.fromValue = [NSNumber numberWithFloat:self.strokeEnd]; // from the current strokeEnd value
animation.toValue = [NSNumber numberWithFloat:_strokeEnd]; //to the end of the path
[CATransaction setCompletionBlock:^{ self.strokeEnd = _strokeEnd }];
[self addAnimation:animation forKey:#"animateStrokeEnd"];
[CATransaction commit];
}
You can pass any value from 0.0f to 1.0f as the value of _strokeEnd.
The setCompletionBlock: ensures that the value you are passing is explicitly set after the animation completes.
Related
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.
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!
this is a simple code form Brad Larson u-tunes course ;)
CABasicAnimation *move = [CABasicAnimation animationWithKeyPath:#"position"];
move.duration = 1.0f;
move.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
move.removedOnCompletion = NO;
move.fillMode = kCAFillModeForwards;
CGPoint currentPosition = l.position;
CGPoint newPosition = CGPointMake(currentPosition.x + 60.0f, currentPosition.y + 60.0f);
move.toValue = [NSValue valueWithCGPoint:newPosition];
[l addAnimation:move forKey:#"position"];
l.position = newPosition;
in the last row i change the position to reflect the final state of layer because animation does not.
But when i execute this code the animation isn't executed and layer move (in 1/4 of sec) to newposition.
someone can explain me how to animate layer's position correctly?
a second question...when i run this code...every subsequent access to property "position" will perform the same animation?
thanks.
Ditch the last line, the l.position = newPosition;. Your animation will already take care of that, and by using that property setter, you’re implicitly giving the layer Core Animation’s default .25-second action.
Also, no, subsequent changes in the position of your layer will not use your 1-second animation. The properties you’re using look pretty much identical to the default animation, though, aside from the duration; a quicker way to accomplish what you’re doing would be something like this.
CGPoint currentPosition = l.position;
CGPoint newPosition = CGPointMake(currentPosition.x + 60.0f, currentPosition.y + 60.0f);
[CATransaction begin];
[CATransaction setAnimationDuration:1.0];
l.position = newPosition;
[CATransaction commit];
It seems as though the layer's properties of my NSView are sometimes not editable/wrong. In the code below, the animation works perfectly, and all appears normal. The output from the NSlogs are always :
anim over opacity = 1.00000
first opacity = 0.50000
current opacity = 0.00000
updated opacity = 0.00000
The first two logs look right, so even at animation did stop, the layer seems to operate normally. However, some time later, when I check the opacity it magically turned to 0. Further wrong, when I set the layer's opacity to 1, and check it immediately after, it still is 0. How is that possible?
I goofed around with setneedsdisplay in the layer and setneedsdisplay:YES in nsview and that
didn't help. Any suggestions are greatly appreciated.
- (void) someSetupAnimationMethod {
aLayer = [CALayer layer];
[theView setWantsLayer:YES];
[theView setLayer:aLayer];
[aLayer setOpacity:0.0];
CABasicAnimation *opacity = [CABasicAnimation animationWithKeyPath:#"opacity"];
opacity.byValue = [NSNumber numberWithFloat:1.0];
opacity.duration = 0.3;
opacity.delegate = self;
opacity.fillMode = kCAFillModeForwards;
opacity.removedOnCompletion = NO;
[opacity setValue:#"opacity done" forKey:#"animation"];
[aLayer addAnimation:opacity forKey:nil];
}
- (void) animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
if([[anim valueForKey:#"animation"] isEqualToString:#"opacity done"]) {
NSLog(#"anim over opacity = %f", aLayer.opacity);
aLayer.opacity = 0.5;
[aLyaer removeAllAnimations];
NSLog(#"first opacity = %f", aLayer.opacity);
}
}
- (void) someLaterMethod {
NSLog(#"current opacity = %f", aLayer.opacity);
aLayer.opacity = 1.0;
NSLog(#"updated opacity = %f", aLayer.opacity);
}
You're breaking a fundamental CALayer/NSView rule by creating a layer-backed view and then trying to manipulate the layer directly.
aLayer = [CALayer layer];
[theView setWantsLayer:YES];
[theView setLayer:aLayer];
When you tell the view to use a layer before calling setLayer:, the view becomes "layer-backed" -- it is simply using the layer as a buffer and all drawing that you want to do should be done through the usual drawRect: and related methods. You are not allowed to touch the layer directly. If you change the order of those calls:
[theView setLayer:aLayer];
[theView setWantsLayer:YES];
You now have a "layer-hosting" view, which means that the view just provides a space on the screen for the layer to be drawn into. In this case, you do your drawing directly into the layer. Since the contents of the layer become the contents of the view, you cannot add subviews or use the view's drawing mechanisms.
This is mentioned in the -[NSView setWantsLayer:] documentation; I also found an old cocoa-dev thread that explains it pretty well.
I'm not certain that this will fix your problem, but as far as I can see you're doing everything else correctly, such as setting the fill mode and updating the value after the animation finishes.
I'm trying to hide a CALayer after a few microseconds and I'm using CABasicAnimation to animate the hide.
At the moment I'm trying to use
[aLayer setHidden:YES];
CABasicAnimation * hideAnimation = [CABasicAnimation animationWithKeyPath:#"hidden"];
[hideAnimation setDuration:aDuration];
[hideAnimation setFromValue:[NSNumber numberWithBool:NO]];
[hideAnimation setToValue:[NSNumber numberWithBool:YES]];
[hideAnimation setBeginTime:0.09];
[hideAnimation setRemovedOnCompletion:NO];
[hideAnimation setDelegate:self];
[alayer addAnimation:hideAnimation forKey:#"hide"];
But when I run this, the layer is hidden immediately, rather than waiting for the desired beginTime.
I'm uncertain about my keyPath as "hidden" but couldn't find any other option and the documentation does state that the hidden property of a CALayer is animatable.
What's the correct way to achieve what I'm looking for?
Try animating the opacity property instead. Go from 1.0 to 0.0 and you should get the effect you want.
From CAMediaTiming.h, it says about beginTime property:
The begin time of the object, in
relation to its parent object, if
applicable. Defaults to 0.
You should use CACurrentMediaTime() + desired time offset.
[hideAnimation setBeginTime:CACurrentMediaTime() + 0.09];
I'm sure this is too late to do the original poster any good, but it may help others. I've been trying to do something similar, except to make the animation implicit when the hidden property is changed. As Tom says, animating opacity doesn't work in that case, as the change to the layer's hidden property seems to take effect right away (even if I delay the animation with beginTime).
The standard implicit action uses a fade transition (CATransition, type = kCATransitionFade), but this operates on the whole layer and I want to perform another animation at the same time, which is not a compatible operation.
After much experimentation, I finally noticed #Kevin's comment above and --- hello! --- that actually works! So I just wanted to call it out so the solution is more visible to future searchers:
CAKeyframeAnimation* hiddenAnim = [CAKeyframeAnimation animationWithKeyPath:#"hidden"];
hiddenAnim.values = #[#(NO),#(YES)];
hiddenAnim.keyTimes = #[#0.0, #1.0];
hiddenAnim.calculationMode = kCAAnimationDiscrete;
hiddenAnim.duration = duration;
This delays the hiding until the end of the duration. Combine it with other property animations in a group to have their effects seen before the layer disappears. (You can combine this with an opacity animation to have the layer fade out, while performing another animation.)
Thank you, Kevin!
swift 4
let keyframeAnimation = CAKeyframeAnimation(keyPath: "hidden")
keyframeAnimation.calculationMode = kCAAnimationDiscrete
keyframeAnimation.repeatCount = 1.0
keyframeAnimation.values = [true, false,true,false,true]
keyframeAnimation.keyTimes = [0.0, 0.25,0.5,0.75, 1.0]
keyframeAnimation.duration = 30.0 //duration of the video in my case
keyframeAnimation.beginTime = 0.1
keyframeAnimation.isRemovedOnCompletion = false
keyframeAnimation.fillMode = kCAFillModeBoth
textLayer.add(keyframeAnimation, forKey: "hidden")
CABasicAnimation *endAnimation = [CABasicAnimation animationWithKeyPath:#"opacity"];
endAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[endAnimation setFromValue:[NSNumber numberWithFloat:1]];
[endAnimation setToValue:[NSNumber numberWithFloat:0.0]];
[endAnimation setBeginTime:AVCoreAnimationBeginTimeAtZero];
endAnimation.duration = 5;
endAnimation.removedOnCompletion = NO;
[alayer addAnimation:endAnimation forKey:nil];