I'm trying to get CALayer working so I can start practicing some animations tricks that I would like to learn. At the moment I want to scale and move things along the z-axis.
Right now I see nothing when I run this code. I would expect to see a black square. But instead nothing is appearing. The code compiles (obviously) and there are no warnings. Is there something I am missing?
I've be trying to learn by reading this tutorial
http://watchingapple.com/2008/04/core-animation-3d-perspective/
But obviously there is a hole in my knowledge somewhere thats left me stumped.
-() addAnImageInTheBackground
{
CALayer *theImage = [CALayer layer];
theImage.backgroundColor = [UIColor blackColor];
theImage.anchorPoint = CGPointZero;
CGRect frame;
frame.origin.x = 10;
frame.origin.y = 10;
frame.size.height = 20;
frame.size.width = 20;
theImage.frame = frame;
theImage.bounds = frame;
CATransform3D transform = CATransform3DIdentity;
transform.m34 = 1.0 / -2000;
theImage.sublayerTransform = transform;
NSNumber *value = [NSNumber numberWithInt:-10];
[theImage setValue:value forKeyPath:#"transform.translation.z"];
[[[self view] layer] addSublayer:theImage];
}
The line theImage.bounds = frame is wrong. You don't want to change the bounds. It's also possible your transform is wrong, but I can't recall enough about transformation matrices to remember what .m34 does ;)
Related
I have several objects of a subclass of uiview, inside the main view of a viewcontroller that I animate infinitely by calling the following method in the class:
- (void)hover:(NSNumber *)upDown {
int sense = [upDown intValue];
[UIView animateWithDuration:0.8 delay:0.1 options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction
animations:^{
CGRect frame = self.frame;
frame.origin.y += (sense==1?1:-1) * 5;
self.frame = frame;
}
completion:^(BOOL finished){
[self hover:[NSNumber numberWithInt:(sense==1?0:1)]];
}];
}
It works great except that in the device, when the home button is pressed, the app semi-freezes (the app eventually goes to background by pressing the home button repeatedly for quite a while) and any interaction with other buttons stops working. It works fine in any other case, i.e. as long as I don't press the home button I can navigate through controllers, press buttons, etc... and on the simulator.
Any ideas?
UPDATE: The culprit seems to be the shadow I'm applying to the views I'm animating
self.layer.shadowColor = [UIColor blackColor].CGColor;
self.layer.shadowOpacity = 1.0;
self.layer.shadowRadius = 5.0;
This seems to be causing some kind of overhead that only affects the app not being able to go into background state???
Anyone encountered this?
UPDATE: In the end I decided to get rid of that code and draw the shadow with Quartz in the drawRect: method. I suspect the problem might have to do with the snapshot that the iPhone takes before going into background mode and the shadow applied to the layer outside the bounds, but it's just a guess.
Basically the problem has to do with this way of obtaining a rounded shape and shadow:
[self.layer setCornerRadius:radius];
self.layer.shadowColor = [UIColor blackColor].CGColor;
self.layer.shadowOpacity = 0.2;
self.layer.shadowRadius = 5;
self.layer.shadowOffset = CGSizeMake(0, 10);
The shadow draws outside the frame of the view and the iPhone's process of going into background state doesn't seem to like that when animating.
As I mention in the update, I got rid of this and used Quartz to draw both shape an shadow inside the frame. There are many posts on how to do this but here it goes anyway:
-(void)drawRect:(CGRect)rect
{
const CGFloat *cComps = CGColorGetComponents([UIColor blackColor]).CGColor;
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat center = rect.size.width / 2.0;
CGFloat radius = center;
CGContextBeginPath(context);
CGContextSetShadowWithColor(context, CGSizeMake(0, 10), 4, [UIColor colorWithWhite:0 alpha:0.1].CGColor);
CGContextAddArc(context, center, center, radius, 0, 2*M_PI, 1);
CGContextSetRGBFillColor(context, cComps[0], cComps[1], cComps[2], 1.0);
CGContextFillPath(context);
}
NOTE: I set the height of the view to +15px to accommodate the shadow at the bottom.
Hope it helps somebody. It certainly drove me mad for a couple of days...
I recently ran into this problem and this post was incredibly helpful in identifying the culprit. Turns out though, that simply setting shouldRasterize on the shadowed layers is sufficient to prevent the freeze. (You should also set rasterizationScale to be the same as the device's main screen's scale.)
All of my shadowed layers were relatively static and weren't being constantly animated, so YMMV.
Your standard shadow adding code:
layer.shadowColor = [UIColor blackColor].CGColor;
layer.shadowOffset = CGSizeMake(0, 1);
layer.shadowOpacity = 0.5;
layer.shadowRadius = 1.0;
This is what you need to add:
layer.rasterizationScale = [[UIScreen mainScreen] scale];
layer.shouldRasterize = YES;
My first thought is that you should implement a BOOL in your .h file (let's call it stopGoing). Then do the following:
- (void)hover:(NSNumber *)upDown {
if(!stopGoing){
int sense = [upDown intValue];
[UIView animateWithDuration:0.8 delay:0.1 options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction
animations:^{
CGRect frame = self.frame;
frame.origin.y += (sense==1?1:-1) * 5;
self.frame = frame;
}
completion:^(BOOL finished){
[self hover:[NSNumber numberWithInt:(sense==1?0:1)]];
}];
} else {
// do something else
}
}
Then, in your IBAction that you have linked to the button, just do this:
-(IBAction)myButton:(id)sender{
stopGoing = YES;
}
That should effectively stop the loop, without being too invasive.
I've managed to get along using [UIView animateWithDuration... to get animations done that I need in my UI. Now I want to move an image along a curved path, and that whole CAAnimation cluster looks pretty daunting to me.
I'd be much obliged if someone could help me fill in the method I wish I could code, which would look like this:
- (void)makeAnImageFlyFrom:(UIImageView *)imageViewA to:(UIImageView *)imageViewB alongPath:(CGMutablePathRef)path duration:(NSTimeInterval)duration {
UIImage *imageToFly = imageViewA.image;
// magic, that i'm too lazy to learn right now goes here
// image flys along the path and gets scaled to match imageViewB.
// then view/layer hierarchy is just as it was, but imageViewB has a new image
// maybe this happens on animationDidStop...
imageViewB.image = imageToFly;
}
Feel free to replace params (like path ref) if you think there's a smarter interface for this kind of method. Thanks in advance.
Ok, a whole Sunday later, here's a method that I think works. I'm still unclear about a few things, noted in comments, but if you need this type of thing, feel free to cut and paste. I promise not call you lazy:
- (void)makeAnImageFlyFrom:(UIImageView *)imageViewA to:(UIImageView *)imageViewB duration:(NSTimeInterval)duration {
// it's simpler but less general to not pass in the path. i chose simpler because
// there's a lot of geometry work using the imageView frames here anyway.
UIImageView *animationView = [[UIImageView alloc] initWithImage:imageViewA.image];
animationView.tag = kANIMATION_IMAGE_TAG;
animationView.frame = imageViewA.frame;
[self addSubview:animationView];
// scale
CABasicAnimation *resizeAnimation = [CABasicAnimation animationWithKeyPath:#"bounds.size"];
[resizeAnimation setFromValue:[NSValue valueWithCGSize:imageViewA.bounds.size]];
[resizeAnimation setToValue:[NSValue valueWithCGSize:imageViewB.bounds.size]];
// build the path
CGRect aRect = [imageViewA convertRect:imageViewA.bounds toView:self];
CGRect bRect = [imageViewB convertRect:imageViewB.bounds toView:self];
// unclear why i'm doing this, but the rects converted to this view's
// coordinate system seemed have origin's offset negatively by half their size
CGFloat startX = aRect.origin.x + aRect.size.width / 2.0;
CGFloat startY = aRect.origin.y + aRect.size.height / 2.0;
CGFloat endX = bRect.origin.x + bRect.size.width / 2.0;
CGFloat endY = bRect.origin.y + bRect.size.height / 2.0;
CGFloat deltaX = endX - startX;
CGFloat deltaY = endY - startY;
// these control points suited the path i needed. your results may vary
CGFloat cp0X = startX + 0.3*deltaX;
CGFloat cp0Y = startY - 1.3*deltaY;
CGFloat cp1X = endX + 0.1*deltaX;
CGFloat cp1Y = endY - 0.5*deltaY;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, startX, startY);
CGPathAddCurveToPoint(path, NULL, cp0X, cp0Y, cp1X, cp1Y, endX, endY);
// keyframe animation
CAKeyframeAnimation *keyframeAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
keyframeAnimation.calculationMode = kCAAnimationPaced;
keyframeAnimation.fillMode = kCAFillModeForwards;
keyframeAnimation.removedOnCompletion = NO;
keyframeAnimation.path = path;
// assuming i need to manually release, despite ARC, but not sure
CGPathRelease(path);
// a little unclear about the fillMode, but it works
// also unclear about removeOnCompletion, because I remove the animationView
// but that seems to be insufficient
CAAnimationGroup *group = [CAAnimationGroup animation];
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
[group setAnimations:[NSArray arrayWithObjects:keyframeAnimation, resizeAnimation, nil]];
group.duration = duration;
group.delegate = self;
// unclear about what i'm naming with the keys here, and why
[group setValue:animationView forKey:#"animationView"];
[animationView.layer addAnimation:group forKey:#"animationGroup"];
}
// clean up after like this
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
UIImageView *imageViewForAnimation = (UIImageView *)[self viewWithTag:kANIMATION_IMAGE_TAG];
// get the imageView passed to the animation as the destination
UIImageView *imageViewB = (UIImageView *)[self viewWithTag:kDEST_TAG];
imageViewB.image = imageViewForAnimation.image;
[imageViewForAnimation removeFromSuperview];
}
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){ }];
Maybe it's because it's late. Whatever the reason I can't figure out why I'm having trouble with a simple CSScrollLayer example I'm trying. I add a 50 pixel Eclipse icon to a view based project and in my initialize method (called from initWithNibName:bundle:) I have this:
-(void) initialize
{
CAScrollLayer *scrollLayer = [CAScrollLayer layer];
scrollLayer.backgroundColor = [[UIColor blackColor] CGColor];
CGRect bounds = self.view.bounds;
scrollLayer.bounds = CGRectMake(0, 0, bounds.size.width, bounds.size.height);
scrollLayer.contentsRect = CGRectMake(0, 0, bounds.size.width + 800, bounds.size.height + 800);
scrollLayer.borderWidth = 2.5;
scrollLayer.borderColor = [[UIColor redColor] CGColor];
scrollLayer.position = CGPointMake(self.view.center.x, self.view.center.y - 20);
scrollLayer.scrollMode = kCAScrollBoth;
[self.view.layer addSublayer:scrollLayer];
UIImage *image = [UIImage imageNamed:#"eclipse32.gif"];
for(int i=0; i<6; i++) {
layer = [CALayer layer];
layer.backgroundColor = [[UIColor blackColor] CGColor];
layer.bounds = CGRectMake(0, 0, 100, 100);
layer.contents = (id)[image CGImage];
layer.position = CGPointMake(layer.bounds.size.width * i, self.view.center.y);
[scrollLayer addSublayer:layer];
}
// [button removeFromSuperview];
// [self.view addSubview:button];
// self.view.userInteractionEnabled = YES;
[image release];
}
The scroll layer shows, the icon is repeated on the layer I have a border around the edge of the screen. Everything is lovely except I can't scroll the icons. I've tried with/without setting scroll mode. I've tried with a single stretched icon that falls off screen. I've tried everything. What am I doing wrong?
CAScrollLayer does not plug into the UIEvent chain so you need to manually add code to modify the scroll offsets on touch events. It is also very tricky to get the scrolling momentum like UIScrollView does. I would strongly recommend re-implementing with UIScrollView -- it will be remarkably simpler. The only other option is to do manual touch event handling, which is painful! I tried to get a nice scroller with CAScrollLayer and gave up after implementing a few awkward scrolling behaviors manually.
The general rule I follow with using CALayer or UIView's is to use CALayer objects when I don't need any user interaction. If the user is going to touch it, use the UIView, it's a very lightweight object so the overhead is negligible.
That said, I've found CAScrollLayer remarkably useless! Why not just use a CALayer and modify the bounds? There's probably some use to it, but it is no replacement for UIScrollView.
Try adding this to your example to scroll the layer programmatically:
- (void)scroll {
[scrollLayer scrollToPoint:scrollPoint];
scrollPoint.x += 10;
[self performSelector:_cmd withObject:nil afterDelay:0.1];
}
and add this at the end of the initialize method:
scrollPoint = CGPointMake(0, 0);
[self scroll];
I'm trying to create a simple iPhone app that displays a picture with a reflection beneath, and I want to rotate the picture around the X axis, using Core Animation.
I've started by creating a new iPhone app using the "View-based Application" template. I added a "Picture.jpg" image file, and then I added this to the view controller:
- (void)awakeFromNib {
CGFloat zDistance = 1500.0f;
// Create perspective transformation
CATransform3D transform = CATransform3DIdentity;
transform.m34 = 1.0f / -zDistance;
// Create perspective transform for reflected layer
CATransform3D reflectedTransform = CATransform3DMakeRotation(M_PI, 1.0f, 0.0f, 0.0f);
reflectedTransform.m34 = 1.0f / -zDistance;
// Create spinning picture
CALayer *spinningLayer = [CALayer layer];
spinningLayer.frame = CGRectMake(60.0f, 60.0f, 200.0f, 300.0f);
spinningLayer.contents = (id)[UIImage imageNamed:#"Picture.jpg"].CGImage;
spinningLayer.transform = transform;
// Create reflection of spinning picture
CALayer *reflectionLayer = [CALayer layer];
reflectionLayer.frame = CGRectMake(60.0f, 360.0f, 200.0f, 300.0f);
reflectionLayer.contents = (id)[UIImage imageNamed:#"Picture.jpg"].CGImage;
reflectionLayer.opacity = 0.4f;
reflectionLayer.transform = reflectedTransform;
// Add layers to the root layer
[self.view.layer addSublayer:spinningLayer];
[self.view.layer insertSublayer:reflectionLayer below:spinningLayer];
// Spin the layers
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
anim.fromValue = [NSNumber numberWithFloat:0.0f];
anim.toValue = [NSNumber numberWithFloat:(2 * M_PI)];
anim.duration = 3.0f;
anim.repeatCount = 1e100f;
[spinningLayer addAnimation:anim forKey:#"anim"];
[reflectionLayer addAnimation:anim forKey:#"anim"];
}
This almost works. The problem is that the spinning of the main picture and of the reflection are not perfectly synchronized. It's very close to perfect, but the edges of the bottom of the picture and of top of the reflection are a few pixels apart. They appear to differ by a few degrees.
On the left side of the screen, the reflection seems to be "ahead" of the picture, and on the right side, the reflection is behind the picture. In other words, it looks like the bottom corners of the top image are pushed toward the screen, while the top corners of the reflection are pulled toward the viewer. This is true both when looking at the front and at the back of the images as they spin.
If I increase zDistance, the effect is less noticeable. If I eliminate the perspective transformations altogether by leaving transform.m34 equal to zero for both layers, then the picture and reflection look to be perfectly synchronized. So I don't think the problem is related to time-synchronization issues.
I suspect my perspective transformations are missing something, but I'm not sure how to determine what's wrong. I think I'm basically doing the same transformation described in this related Stack Overflow question.
Can someone help?
I think I may have found an answer to my question. In my original code, I created the reflection by placing it underneath the picture, and applying the flip and perspective. The right way to do it seems to be to place the reflection at the same position as the picture, apply translation and rotation transforms to get it to the right place, and then apply the perspective transformation.
When animating the spin, it is necessary to spin the reflection in the opposite direction of the picture's animation.
So, here's code that works:
- (void)awakeFromNib {
CGFloat zDistance = 1500.0f;
// Create perspective transformation
CATransform3D transform = CATransform3DIdentity;
transform.m34 = 1.0f / -zDistance;
// Create perspective transform for reflected layer
CATransform3D reflectedTransform = CATransform3DMakeTranslation(0.0f, 300.0f, 0.0f);
reflectedTransform = CATransform3DRotate(reflectedTransform, M_PI, 1.0f, 0.0f, 0.0f);
reflectedTransform.m34 = 1.0f / -zDistance;
// Create spinning picture
CALayer *spinningLayer = [CALayer layer];
spinningLayer.frame = CGRectMake(60.0f, 60.0f, 200.0f, 300.0f);
spinningLayer.contents = (id)[UIImage imageNamed:#"Picture.jpg"].CGImage;
spinningLayer.transform = transform;
// Create reflection of spinning picture
CALayer *reflectionLayer = [CALayer layer];
reflectionLayer.frame = CGRectMake(60.0f, 60.0f, 200.0f, 300.0f);
reflectionLayer.contents = (id)[UIImage imageNamed:#"Picture.jpg"].CGImage;
reflectionLayer.opacity = 0.4f;
reflectionLayer.transform = reflectedTransform;
// Add layers to the root layer
[self.view.layer addSublayer:spinningLayer];
[self.view.layer insertSublayer:reflectionLayer below:spinningLayer];
// Spin the layers
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
anim.fromValue = [NSNumber numberWithFloat:0.0f];
anim.toValue = [NSNumber numberWithFloat:(2 * M_PI)];
anim.duration = 3.0f;
anim.repeatCount = 1e100f;
[spinningLayer addAnimation:anim forKey:#"anim"];
CABasicAnimation *reflectAnim = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
reflectAnim.fromValue = [NSNumber numberWithFloat:0.0f];
reflectAnim.toValue = [NSNumber numberWithFloat:(-2 * M_PI)];
reflectAnim.duration = 3.0f;
reflectAnim.repeatCount = 1e100f;
[reflectionLayer addAnimation:reflectAnim forKey:#"reflectAnim"];
}
I'm not sure exactly why this works. It makes intuitive sense to me that placing the reflection at the same position as the original and then transforming it somehow puts the two images into the same "transformation space", but I'd appreciate it if someone could explain the mathematics behind this.
The full project is available at GitHub: https://github.com/kristopherjohnson/perspectivetest
You can use a CAAnimationGroup object to run two animations on the same time space. That should take care of any synchronization issues.
EDIT: removed silly code.