I am messing around with a basic CAAnimation example where Im just trying to get a CALayer to rotate in a subclass of UIView. I start by making a new CALayer with a red background and inserting it into self.layer. I then make a CAAnimation that is supposed to be triggered on opacity changes and apply it to the CALayer, after which it spins as expected.
Weird thing is that if I try to apply the exact same animation using performSelector:withObject:afterDelay:, the CALayer no longer animates, although its opacity still changes. Even weirder is that if I use performSelector:withObject:afterDelay: to animate self.layer using pretty much the same code, it works.
Any ideas as to what's going on? Here is my code:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
point = [[CAGradientLayer alloc] init];
point.backgroundColor = [UIColor redColor].CGColor;
point.bounds = CGRectMake(0.0f, 0.0f, 30.0f, 20.0f);
point.position = CGPointMake(100.0f, 100.0f);
[self.layer addSublayer:point];
[point release];
[self performSelector:#selector(test) withObject:nil afterDelay:2];
}
return self;
}
-(void)test {
[self spinLayer:point];
// [self spinLayer:self.layer]; works here
}
-(CAAnimation*)animationForSpinning {
CATransform3D transform;
transform = CATransform3DMakeRotation(M_PI/2.0, 0, 0, 1.0);
CABasicAnimation *basicAnimation = [CABasicAnimation animationWithKeyPath:#"transform"];
basicAnimation.toValue = [NSValue valueWithCATransform3D:transform];
basicAnimation.duration = 5;
basicAnimation.cumulative = NO;
basicAnimation.repeatCount = 10000;
return basicAnimation;
}
-(void)spinLayer:(CALayer*)layer {
CAAnimation *anim = [self animationForSpinning];
[layer addAnimation:anim forKey:#"opacity"];
layer.opacity = 0.6;
}
and this is my interface
#interface MyView : UIView {
CALayer *point;
}
-(void)test;
-(void)spinLayer:(CALayer*)layer;
-(CAAnimation*)animationForSpinning;
#end
Note: I am instantiating this view with a frame equal to [UIScreen mainScreen].applicationFrame from within the app delegate.
Change this:
[layer addAnimation:anim forKey:#"opacity"];
into this:
[layer addAnimation:anim
forKey:#"notOpacity!"];
What happens is that implicit animation layer.opacity = 0.6 overwrites you explicit animation. This happens because Core Animation uses property names when adding implicit animations.
(If you move layer.opacity = 0.6 above [layer addAnimation:anim forKey:#"opacity"]; it will also work, but layer.opacity will not get animated.)
And why it works without delay: because implicit animations are "enabled" only if layer is in the tree and been displayed.
Related
I have a UIView that draws a circle inside it using the following code:
-(void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, myColor);
CGContextFillEllipseInRest(context, myRect);
}
I want to animate this circle to a grey color... How can I achieve this?
-(void)fadeToGrey {
// What goes here?
}
You should use CAShapeLayer as a layer class and do your drawing inside setLayerProperties: method. drawRect: will not be overridden. Here is my sample code with comments of custom UIView with attaching color animation method.
//Returns the class used to create the layer for instances of this class.
//it is CALayer class by default.
+ (Class)layerClass
{
return [CAShapeLayer class];
}
//Lays out subviews.
- (void)layoutSubviews
{
[self setLayerProperties];
}
- (void)setLayerProperties {
//The view’s Core Animation layer used for rendering.
CAShapeLayer *layer = (CAShapeLayer *)self.layer;
// Color Declarations
UIColor* strokeColor = [UIColor redColor];
// Some sample Bezier Drawing. In my case it is a triangle.
// However, you can draw circle by using `+bezierPathWithRoundedRect:cornerRadius:`
UIBezierPath* bezierPath = [UIBezierPath bezierPath];
[bezierPath moveToPoint: CGPointMake(0.0, self.frame.size.height)];
[bezierPath addLineToPoint: CGPointMake(self.frame.size.width/2, 0.0)];
[bezierPath addLineToPoint: CGPointMake(CGRectGetWidth(self.frame), CGRectGetHeight(self.frame))];
[bezierPath addLineToPoint: CGPointMake(0.0, CGRectGetHeight(self.frame))];
bezierPath.miterLimit = 8;
bezierPath.lineJoinStyle = kCGLineJoinBevel;
layer.path = bezierPath.CGPath;
layer.fillColor = strokeColor.CGColor;
}
//here is the magic :)
- (void)attachColorAnimationToColor:(UIColor *)color {
//"fillColor" property of CAShapeLayer's instance is animatable
//(thus we have overridden `layerClass` method btw), that leads us to create
//CABasicAnimation instance with fromValue (initial color), toValue (final color).
CABasicAnimation *animation = [self animationWithKeyPath:#"fillColor"];
animation.fromValue = (__bridge id)(((CAShapeLayer *)self.layer).fillColor);
animation.toValue = (__bridge id)color.CGColor;
[self.layer addAnimation:animation forKey:animation.keyPath];
}
//setting properties of color animation
- (CABasicAnimation *)animationWithKeyPath:(NSString *)keyPath {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:keyPath];
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion=NO;
animation.repeatCount = 1;
animation.duration = 0.3f;
return animation;
}
You will use it by calling attachColorAnimationToColor: like this:
//your custom view declaration
YourView *yourView = [[YourView alloc]initWithFrame:CGRectMake(...)];
[self.view addSubview:yourView];
[yourView attachColorAnimationToColor:[UIColor lightGrayColor]];
CAShape Layer Class Reference to fillColor property
Feel free to ask if something goes wrong.
UPDATE
Our layer fillColor is red. So if you fire animation to redColor, that means that you want animation from 'red' to 'red'. Create method reverse animation, where toValue is our stroke color of shape. (Animation does not update it, it stays red always). Here is sample that was tested now :)
-(void)attachReverseAnimationFromColor:(UIColor*)color{
CABasicAnimation *animation = [self animationWithKeyPath:#"fillColor"];
animation.toValue = (__bridge id)(((CAShapeLayer *)self.layer).fillColor);
animation.fromValue = (__bridge id)color.CGColor;
[self.layer addAnimation:animation forKey:animation.keyPath];
}
PS Happy Coding.
I want to crossfade between two different UIImages but for a reason i cannot figure out my first UIImage stays when the second one fades in.
Here is the sourcecode of my animation (it does a lot more than just crossfading, but the crossfading is my only problem right now).
I need all of the three animations listed below to be executed at the same time.
CGMutablePathRef thePath = CGPathCreateMutable();
CGPathMoveToPoint(thePath,NULL,startPoint.x, startPoint.y);
CGPathAddLineToPoint(thePath, NULL, endPoint.x, endPoint.y);
CAKeyframeAnimation *positionAnimation =[CAKeyframeAnimation animationWithKeyPath:#"position"];
positionAnimation.path=thePath;
positionAnimation.duration=ti_duration;
positionAnimation.repeatCount=0;
positionAnimation.delegate = self;
positionAnimation.autoreverses=NO;
positionAnimation.fillMode=kCAFillModeForwards;
positionAnimation.removedOnCompletion = NO;
[self.layer addAnimation:positionAnimation forKey:s_direction];
CABasicAnimation *crossFade = [CABasicAnimation animationWithKeyPath:#"contents"];
crossFade.duration = ti_duration;
crossFade.fromValue = (id)img_startImage.CGImage;
crossFade.toValue = (id)img_transferImage.CGImage;
[self.iv_image.layer addAnimation:crossFade forKey:#"animateContentsToTransferState"];
[self.iv_image setImage:img_transferImage];
CAAnimationGroup *theGroup = [CAAnimationGroup animation];
theGroup.duration = ti_duration;
theGroup.repeatCount = 0;
theGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
theGroup.animations = [NSArray arrayWithObjects:/*positionAnimation,*/ crossFade, nil]; // you can add more
[self.layer addAnimation:theGroup forKey:#"move"];
[UIView beginAnimations:#"changeSize" context:nil];
[UIView setAnimationDuration:duration];
self.bounds = CGRectMake(self.bounds.origin.x, self.bounds.origin.y, self.transferSize.width, self.transferSize.height);
[UIView commitAnimations];
The animation method containing this sourcecode is in a subclass of UIView. This subclass contains several UIImages and one UIImageView where the image to be displayed is contained in.
Have i forgotten some essential thing or why is my first image not fading away?
Can it be because it is animated from some other animation at the same time?
I hope someone can help me with this.
Greets
Maverick
Think of CAAnimations as operations applied to the CALayer. The animations current state do not change the layers original contents.
When the original state seems to snap back at the end of the animation it is actually just the animations operation being removed, and the real layers state is being revealed again.
What you need to do is to register a delegate for your animation, and change the actual self.layer.contents when the animation ends.
First:
crossFade.delegate = self;
Then implement:
- (void)animationDidStop:(CABasicAnimation *)animation finished:(BOOL)flag {
self.layer.contents = animation.toValue;
}
Try this code...It worked for me.
-(void)crossFadeImage{
CABasicAnimation *crossFade = [CABasicAnimation animationWithKeyPath:#"contents"];
crossFade.duration = 2.0;
crossFade.fromValue = img1.CGImage;
crossFade.toValue = img2.CGImage;
[self.background.layer addAnimation:crossFade forKey:#"animateContents"];
self.background.image = img2;
}
I'm trying to slide down an image in an UIImageView, but I don't know which combination of UIContentMode and animation property is the right one to make that happen.
The image should always have the same size and should not be streched... all I want is, that nothing is visible first and then the frame extends and reveals the image.
Maybe it's easier if you see what I mean:
So, it sounds rather easy, but what UIContentMode should I use for the UIImageView and what property should I animate? Thank you!
I took your lead and made a screencast as well. Was this what you had in mind?
I put the animation repeating indefinitely so it would be easier to capture with a video, but it can be started at the press of a button, as well as frozen in place, showing the popover and its contents, until reversed to be hidden again.
I used Core Animation for that, instead of animating a UIView, since I wanted to use the mask property of CALayer to hide the popover and reveal it with a sliding animation.
Here is the code I used (same as in the video):
- (void)viewDidLoad
{
// Declaring the popover layer
CALayer *popover = [CALayer layer];
CGFloat popoverHeight = 64.0f;
CGFloat popoverWidth = 200.0f;
popover.frame = CGRectMake(50.0f, 100.0f, popoverWidth, popoverHeight);
popover.contents = (id) [UIImage imageNamed:#"popover.png"].CGImage;
// Declaring the mask layer
CALayer *maskLayer = [CALayer layer];
maskLayer.frame = CGRectMake(0, - popoverHeight, popoverWidth, popoverHeight);
maskLayer.backgroundColor = [UIColor colorWithRed:1.0f green:1.0f blue:1.0f alpha:1.0f].CGColor;
// Setting the animation (animates the mask layer)
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position.y"];
animation.byValue = [NSNumber numberWithFloat:popoverHeight];
animation.repeatCount = HUGE_VALF;
animation.duration = 2.0f;
[maskLayer addAnimation:animation forKey:#"position.y"];
//Assigning the animated maskLayer to the mask property of your popover
popover.mask = maskLayer;
[self.view.layer addSublayer:popover];
[super viewDidLoad];
}
NOTE: You have to import the QuartzCore framework into your project and write this line in your header file: #import <QuartzCore/QuartzCore.h>.
Tells if this works for you or if you need any more help setting this up.
Try this code.
Consider the UIImageView as imageView.
imageView.contentMode = UIViewContentModeScaleAspectFill;
CGRect imageRect = imageView.frame;
CGRect origImgRect = imageRect;
imageRect.size.height = 5;
imageView.frame = imageRect;
[UIView animateWithDuration:2.0
animations:^{imageView.rect = origImgRect;}
completion:^(BOOL finished){ }];
I have a problem very similar to this one :
CATransaction Not Animating
I'm just trying to animate a view layer using CATransaction. My problem is that the transform is applied to the view immediatly.
I tried to perform the animation using performSelector:withObject:afterDelay: without success.
Here is my code :
- (void)viewDidLoad {
[super viewDidLoad];
view = [[[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)] autorelease];
view.backgroundColor = [UIColor blackColor];
[self.view addSubview:view];
[self performSelector:#selector(animateView) withObject:nil afterDelay:0.1];
}
-(void) animateView {
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:3.0f] forKey:kCATransactionAnimationDuration];
CALayer *layer = view.layer;
layer.position = CGPointMake(20,
300);
CATransform3D transform = CATransform3DMakeScale(2.0f, 2.0f, 1.0f);
transform = CATransform3DRotate(transform, acos(-1.0f)*1.5f, 1.5f, 1.5f, 1.5f);
layer.transform = transform;
[CATransaction commit];
}
Does anybody knows what is going wrong ?
Thanks, Vincent.
when animating a view's backing layer, you need to be inside a UIView animation block, not just a CATransaction:
[UIView beginAnimations:nil context:NULL];
// ... your animation code
[UIView commitAnimations];
or you could use CAAnimationGroup and group the scale and rotation animations like this..
CAAnimationGroup *aniGroup = [CAAnimationGroup animation];
// setup aniGroup properties
CABasicAnimation *scaleAnimation = [CABasicAnimation defaultValueForKey:#"transform.scale"];
scaleAnimation.fromValue = [NSNumber numberWithFloat:0.0];
scaleAnimation.toValue = [NSNumber numberWithFloat:1.0];
// setup scaleAnimation properties
CABasicAnimation *rotateAnimation = [CABasicAnimation defaultValueForKey:#"transform.rotate"];
// setup rotateAnimation properties
// ....
aniGroup.animations = [NSArray arrayWithObjects:rotateAnimation, scaleAnimation, nil];
[self.layer layoutIfNeeded];
[self.layer addAnimation:aniGroup forKey:#"myAnimation"];
Something like that.... Might not be exactly copy, paste, and build-able. ;)
Hope this helps!
I'm brand new in iPhone animation feature. I wrote a simple testing program to spin a red rectangle; however, the animation doesn't act at all. What's wrong with my code? Thanks a lot...
//.h
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#define DegreesToRadians(X) (M_PI*X/180.0)
//.m
- (void)viewDidLoad {
[super viewDidLoad];
CGRect theRect = CGRectMake(0,0,100,100);
UIView *theBox = [[UIView alloc] initWithFrame:theRect];
theBox.backgroundColor = [UIColor redColor];
[self.view addSubview:theBox];
CALayer *theLayer = [theBox layer];
[self spinLayer:theLayer];
}
-(CAAnimation *)animationForSpinning{
float radians = DegreesToRadians(360);
CATransform3D theTransform = CATransform3DMakeRotation(radians, 0, 0, 1.0);
CABasicAnimation *theAnimation = [CABasicAnimation animationWithKeyPath:#"transform"];
theAnimation.toValue = [NSValue valueWithCATransform3D:theTransform];
theAnimation.duration = 2; // 2 seconds
theAnimation.repeatCount = 10000;
theAnimation.cumulative = YES;
return theAnimation;
}
-(void)spinLayer:(CALayer *)theLayer{
CAAnimation *spinningAnimation = [self animationForSpinning];
[theLayer addAnimation:spinningAnimation forKey:#"opacity"];
// trigger the animation
theLayer.opacity = 0.99;
}
Your animation is not running because Core Animation sees a transform of 360 degrees being the same as your original image, so it does nothing.
The easiest way to handle a 360 degree rotation is described in this answer to this question, where you set up your CABasicAnimation to animate the transform.rotation.z property from 0 to 2 * pi radians.