Animating a shape with CoreAnimation - iphone

I approaching core animation and drawing empirically.
I am trying to animate a simple shape; the shape in question is formed by 3 lines plus a bezier curve. A red line is also drawn, to show the curve control points.
alt text http://img.skitch.com/20091119-1ufar435jdq7nwh8pid5cb6kmm.jpg
My main controller simply adds this subview and calls the adjustWave method whenever touchesEnd.
Here is the code for my shape drawing class. As you see the class has one property, cp1x (the x of the bezier control point 1). This is the value I would like to animate. Mind, this is a dumb attempt ...
- (void)drawRect:(CGRect)rect {
float cp1y = 230.0f;
float cp2x = 100.0f;
float cp2y = 120.0f;
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextClearRect(ctx, rect);
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 10.0f, 200.0f);
CGPathAddCurveToPoint (path, NULL, cp1x, cp1y, cp2x, cp2y, 300.0f, 200.0f);
CGPathAddLineToPoint(path, NULL, 300.0f, 300.0f);
CGPathAddLineToPoint(path, NULL, 10.0f, 300.0f);
CGPathCloseSubpath(path);
CGContextSetFillColorWithColor(ctx, [UIColor blueColor].CGColor);
CGContextAddPath(ctx, path);
CGContextFillPath(ctx);
// Drawing a line from control points 1 and 2
CGContextBeginPath(ctx);
CGContextSetRGBStrokeColor(ctx,1,0,0,1);
CGMutablePathRef cp1 = CGPathCreateMutable();
CGPathMoveToPoint(cp1, NULL, cp1x, cp1y);
CGPathAddLineToPoint(cp1, NULL, cp2x, cp2y);
CGPathCloseSubpath(cp1);
CGContextAddPath(ctx, cp1);
CGContextStrokePath(ctx);
}
- (void)adjustWave {
[UIView beginAnimations:#"movement" context:nil];
[UIView setAnimationDelegate:self];
[UIView setAnimationWillStartSelector:#selector(didStart:context:)];
[UIView setAnimationDidStopSelector:#selector(didStop:finished:context:)];
[UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
[UIView setAnimationDuration:3.0f];
[UIView setAnimationRepeatCount:3];
[UIView setAnimationRepeatAutoreverses:YES];
cp1x = cp1x + 20.0f;
[UIView commitAnimations];
}
The shape doesn't change. If, conversely, I take out the CA code and add a simple `[self setNeedsDisplay] in the last method the shape changes, but obviously without CA.
Can you help sme? I am sure I am making a very basic mistake hereā€¦
Thanks in advance,
Davide

If you are writing this for iPhone OS 3.x (or Snow Leopard), the new CAShapeLayer class should let you do this kind of animation pretty easily. If you have a path that maintains the same number of control points (like in your case), you can set that path to the CAShapeLayer, then animate the path property from that starting value to your final path. Core Animation will perform the appropriate interpolation of the control points in the path to animate it between those two states (more, if you use a CAKeyframeAnimation).
Note that this is a layer, so you will need to add it as a sublayer of your UIView. Also, I don't believe that the path property implicitly animates, so you may need to manually create a CABasicAnimation to animate the change in shape from path 1 to path 2.
EDIT (11/21/2009): Joe Ricioppo has a nice writeup about CAShapeLayer here, including some videos that show off these kinds of animations.

You won't be able to animate your shape with CA. You can only animate layer properties in CA, not custom properties.
CA can be fast because it doesn't have to call back to your code to perform animations, if you think about how it is implemented, it basically is just a thread running GL primitives operations with CALayer frames and contents.
CA animations are sets of instructions for CA to run on its background thread, fire and forget (plus callback at animation completion), not callbacks to your code.
Animating the contents of a layer would require CA to call you back to draw at each frame, or to let you instruct CA about how to draw. Neither of those happen.

Related

Animating Circle's endAngle with CABasicAnimation

I have some UIView with drawrect code for drawing pie:
- (void)drawRect:(CGRect)rect
{
CGRect parentViewBounds = self.bounds;
CGFloat x = CGRectGetWidth(parentViewBounds)/2;
CGFloat y = CGRectGetHeight(parentViewBounds)/2;
// Get the graphics context and clear it
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextClearRect(ctx, rect);
// define line width
CGContextSetLineWidth(ctx, 4.0);
CGContextSetFillColor(ctx, CGColorGetComponents( [someColor CGColor]));
CGContextMoveToPoint(ctx, x, y);
CGContextAddArc(ctx, x, y, 100, radians(270), radians(270), 0); // 27
CGContextClosePath(ctx);
CGContextFillPath(ctx);
}
But when I try to animate "endAngle" property after drawing, it doesn't work:
CABasicAnimation *theAnimation;
theAnimation=[CABasicAnimation animationWithKeyPath:#"endAngle"];
theAnimation.duration=1;
theAnimation.repeatCount=1;
theAnimation.autoreverses=NO;
theAnimation.fromValue=[NSNumber numberWithFloat:270];
theAnimation.toValue=[NSNumber numberWithFloat:60];
[[self.layer presentationLayer] addAnimation:theAnimation forKey:#"animateLayer"];
Where I'm making a mistake?
If you want to make an animation then you should look into using Core Animation for the drawing. It makes animation much simpler.
Have a look at this great tutorial on making a custom animatable pie chart with Core Animation. I'm sure that you can modify it to get what you want.
If it seems to complicated to you then you can have a look at my answer to "Draw part of a circle" which draws a pie chart shape by making a very wide stroke of a circle.
Several things. You can't use a CAAnimation to animate drawing that you do in a drawRect method like that.
Second, if you did use a CAShapeLayer, you could animate changes to the path that's installed in the layer, but you need to make sure that the path has the same number and type of control points for all steps in the animation.
CGPath arc commands use different numbers of control points to draw different angles. Thus, you can't change the angle of the arc and animate it.
What you have to do is to install a path that's the full length of your arc, and then animate changes to the shape layer's strokeStart and/or strokeEnd properties. Those properties cause the path to omit the first part/last part of the path from the drawing. I daresay the tutorial David linked to on the animatable pie chart uses that technique.

Animate custom UIView

I have a simple custom UIView (a rectangle) that is implemented with drawRect.
The view is drawn from two values; currentValue and maxValue
From drawRect:
The height of the rect represents how much currentValue is of maxValue:
if(currentValue > maxValue)
currentValue = maxValue;
float scale = currentValue/maxValue;
//Draw the rect
CGContextBeginPath(context);
CGContextMoveToPoint(context, self.bounds.origin.x, self.bounds.size.height);
CGContextAddLineToPoint(context, self.bounds.size.width, self.bounds.size.height);
CGContextAddLineToPoint(context, self.bounds.size.width, self.bounds.size.height-(self.bounds.size.height*scale));
CGContextAddLineToPoint(context, self.bounds.origin.x, self.bounds.size.height-(self.bounds.size.height*scale));
CGContextClosePath(context);
CGContextSetRGBFillColor(context, 1.0, 0.0, 0.0, 1.0);
CGContextFillPath(context);
CGContextStrokePath(context);
I've setup a ViewController with a UISlider that changes the currentValue and each time it's changed setNeedsDisplay gets called. This part works just fine. The rect changes height when the slider is used, exactly as planned.
My question is.. I would like this transition between heights to be animated. What's the best way to do it?
Thanks
Instead of having UIView with custom drawing your can add CAShapeLayer object to your view's layer. CAShapeLayer class allows to specify path to draw and draw attributes - and also changes for those properties are easily animatable.
If your view is just a rect filled with solid color as in question your second option is either adjust view's frame or set appropriate affine transform to get required heights - both those properties are also easily animatable using animation methods in UIView class.
If you really just want a simple shape that can be drawn by a Core Animation layer (like CAShapeLayer or CAGradientLayer), you should just use a Core Animation layer and a CABasicAnimation to animate the layer's frame.
If you plan to draw something more complex, and you want to animate changes to the shape, then you need to do more work. You need to give your object properties or instance variables to store the current and final values, and maybe to store the velocity of the value. You need to create a CADisplayLink object and use it to drive calls to your animation method. Your animation method should update the current value based on how much time has passed and then call setNeedsDisplay.
You'll probably want to do this with Core Animation. The relevant docs are on Apple's developer site. There is also a good source of links on Core Animation with iOS on this other SO question.

How to animate fill color of path drawn with CGContextDrawPath, etc in drawRect:?

I wonder if is possible to animate the fill color of a path I have drawn using CoreGraphics?
I am drawing this:
Simple drawing with Quartz - Core Graphics
and I want to change its fill color from white to let's say gray.
Is this possible?
I know the existence of view.layer.content property but Is this useful here? Though, I am not sure how can I use it in this case.
Thanks in advance.
Update
I am trying this approach (its buggy, hence I can tell if its going to work)
basically I am creating a CGImageRef and pass it to self.layer.contents which is animatable
using UIView animations but .... I am getting strange results, besides is not animating.
int bitsPerComponent = 8;
int channels = 4;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
void *data = malloc(self.bounds.size.width*self.bounds.size.height*channels);
CGContextRef context = CGBitmapContextCreate(data, //pointer to data
self.bounds.size.width, //width
self.bounds.size.height, //height
bitsPerComponent, //bitsPerComponent
self.bounds.size.width*channels,//bytesPerRow
colorSpace, //colorSpace
kCGImageAlphaPremultipliedFirst); //bitmapInfo
//method that uses below link's code
[self _drawBackgroundInContext:context color:UIColorFromMandalaBoxType(type)];
CGDataProviderRef dataProvider = CGDataProviderCreateWithData(NULL, //info, NULL
data, //pointer to data
self.bounds.size.width*self.bounds.size.height*channels, //number of bytes
NULL); //release callback
CGImageRef image = CGImageCreate(self.bounds.size.width, //width
self.bounds.size.height, //height
bitsPerComponent, //bitsPerComponent
bitsPerComponent*channels, //bitsPerPixel
self.bounds.size.width*channels, //bytesPerRow
colorSpace, //colorSpace
kCGImageAlphaPremultipliedFirst, //bitmapInfo
dataProvider, NULL, false, kCGRenderingIntentDefault);
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:1.5f];
self.layer.contents = (id)image;
[UIView commitAnimations];
CGImageRelease(image);
CGDataProviderRelease(dataProvider);
CGContextRelease(context);
free(data);
CGColorSpaceRelease(colorSpace);
Is logic OK?, and where is the mistake here? ;(
Edited answer:
You can't animate stuff in -drawRect:. If you're trying to animate the color of a shape, you may want to look at CAShapeLayer. It allows you to specify a CGPathRef that it should draw, as well as stroke/fill colors, and all these properties are animatable. One thing to bear in mind, though, is that this isn't terribly efficient at its job. If your shape is not animating all the time, you may want to set shouldRasterize to YES. This will significantly speed up the rendering as long as the layer is not animating. If you do this, be careful as there are bugs relating to shouldRasterize. One that springs to mind is if you set the opacity to 0, and later make it non-0, shouldRasterize has a tendency to draw nothing at all because it cached the drawing at the 0 opacity.
Original answer:
If you're living in CoreGraphics entirely, you can use CGContextSetFillColorWithColor(), giving it the context and a CGColorRef (e.g. CGContextSetFillColorWithColor(ctx, [UIColor grayColor].CGColor). If you're trying to adjust the fill color of the "current" graphics context, you can also just use the slightly simpler [[UIColor grayColor] setFill].
Edit: After looking at your link, you just need to change the line that says
CGContextSetFillColorWithColor(context, [UIColor white].CGColor);
to use whatever color you want. In this case it would be
CGContextSetFillColorWithColor(context, [UIColor grayColor].CGColor);
I don't think you can. Quartz is meant for drawing static things like buttons, it's not good at real-time drawing.
You might look at CALayer and Core Animation. You might be able to do something like this (untested):
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration: 0.8];
self.view.layer.backgroundColor = [UIColor grayColor];
[UIView commitAnimations];
CALayer's backgroundColor is "animatable". This might require some changes to your drawRect: method - like doing a fill with [UIColor clearColor] and/or setting view's opaque property to NO, etc.
Just an idea - but it should be doable.
I had to do something similar but a bit more involved. For that I ended up using a second thread and a kind of Producer/Consumer pattern. I think that that would overkill for your needs, but it is another option.

Drawing and Animating with UIView or CALayer?

i am trying to implement a graph which a uiview draws. There are 3 UIViews to animate a left/right slide. The problem is, that i can't cancel the UIView animation. So I replaced the UIViews by CALayer. Now, the question is if CALayer is appicable for this? Is it normal to draw on a CALayer like this? And why is a CALayer so slow when I manipulate the frame properties.
CGRect frame = curve.frame;
frame.origin.x -= diffX;
[curve setFrame:frame];
Is there a alternativ?
P.S. I am a german guy. Sorry for mistakes.
I got the animation with CATransaction, but now I will animate a x move with CABasicAnimation. That's no problem expect that the position of the layer go back to the previous x.
CABasicAnimation *theAnimation;
theAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
theAnimation.delegate = self;
theAnimation.duration = 1.0;
theAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
CGPoint position = [[curves objectAtIndex:i]position];
position.x = [[curves objectAtIndex:i]position].x - diffX;
[theAnimation setToValue:[NSValue valueWithCGPoint:position]];
[[curves objectAtIndex:i] addAnimation:theAnimation forKey:[NSString stringWithFormat: #"translate.x.%d", index]];
The position changes the position (e.g. xStart = 100, xEnd = 200), but when the animation ends the layer goes back to the beginning x (e.g. x = 100).
Is that normal? How can I solve this problem, that the end position doesn't change anymore?
I tried to changed the removeOnComplete property but that didn't effect.
Hope for help.
Markus
Not sure what you mean by 'slow', but setting the frame of a CALayer in this way uses 'implicit animation'. That is, it will animated the transition from the old frame to the new frame. You can turn this off:
[CATransaction begin];
[CATransaction setValue: (id) kCFBooleanTrue forKey: kCATransactionDisableActions];
[curve setFrame:frame];
[CATransaction commit];
However, this is usually considered an advantage of CALayer. You way want to just use UIViews here, which will not, by default, animate transitions such as this.
Instead of setting the destination position in theAnimation, just set the position property of the thing you want to move.
When you addAnimation:theAnimation, you're setting the "visual style" of any changes to the keyPath property you specified.
When you change the position of the object that the animation is attached to, say from (0,0) to (500,500), CoreAnimation will animate the change for you. The theAnimation object doesn't need the start and end positions, since the underlying object has them.

Can i move the origin when doing a rotation transform in Quartz 2D for the iPhone?

Sorry if this is obvious or covered elsewhere, but i've been googling all day and haven't found a solution that actually worked.
My problem is as follows: I am currently drawing an image in a full screen UIView, for examples sake we'll say the image is in the bottom right corner of the UIView. I'd like to do a rotation transform(CGAffineTransformMakeRotation) at the center of that image, however, by default the rotation command rotates around the center of the UIView it self. As a result, my image moves around the screen when i rotate instead of it staying in place and rotating around its own center.
From what i've gathered, i need to translate my context so that the origin(center of the UIView) is at the center of my image, Rotate, and then restore the context by translating back to the original spot.
The following is the closest thing i've gotten to work, but the problem is that while the image is rotating, it moves downward while it's rotating. I think this is caused by animation tweening the 1st step translate and 3rd step translate instead of just realizing that the beginning and end point on the translates would be the same...
// Before this i'd make a call to a function that draws a path to a passed in context
CGAffineTransform inverseTranslation = CGAffineTransformMakeTranslation( transX, transY );
CGAffineTransform translation = CGAffineTransformMakeTranslation( -transX, -transY );
CGAffineTransform rot = CGAffineTransformMakeRotation( 3.14 );
CGAffineTransform final = CGAffineTransformConcat( CGAffineTransformConcat( inverseTranslation, rot ), translation );
// Then i apply the transformation animation like normal using self.transform = final etc etc
I've also tried stuff like CGContextTranslateCTM and CGContextSaveGState/UIGraphicsPushContext, but these seem to have little effect.
I've been fighting with this for days and my current solution seems close, but i have no clue how to get rid of that translating tweening. Does anyone have a solution for this or a better way to go about this?
[update]
For the time being i'm drawing my image centered at the UIview's center and then setting the UIView.center property to the origin i'd like to rotate and then doing the rotate command. Seems like a bit of a hack, but until i can get the real translates working it's my only choice.
Thanks!
duncanwilcox' answer is the right one, but he left out the part where you change the anchor of the view's layer to the point you want to rotate around.
CGSize sz = theview.bounds.size;
// Anchorpoint coords are between 0.0 and 1.0
theview.layer.anchorPoint = CGPointMake(rotPoint.x/sz.width, rotPoint.y/sz.height);
[UIView beginAnimations:#"rotate" context:nil];
theview.transform = CGAffineTransformMakeRotation( 45. / 180. * M_PI );
[UIView commitAnimations];
This is documented here: http://developer.apple.com/documentation/Cocoa/Conceptual/CoreAnimation_guide/Articles/Layers.html
This is also an option: a simple change of basis ;-)
CGAffineTransform transform = CGAffineTransformMakeTranslation(x, y);
transform = CGAffineTransformRotate(transform, angle);
transform = CGAffineTransformTranslate(transform,-x,-y);
where (x,y) is the rotation center you like
Rotation happens around the anchorPoint of the view's layer. The default for the anchorPoint is the center (0.5, 0.5), so the rotation alone without the translations should suffice.
Did a quick test and this works for me:
[UIView beginAnimations:#"rotate" context:nil];
self.transform = CGAffineTransformMakeRotation( 45. / 180. * 3.14 );
[UIView commitAnimations];
If you don't want an animation to occur, but just set the new rotation, you can use this:
CGFloat newRotation = 3.14f
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.0]; // no tweening
CGAffineTransform transform = CGAffineTransformMakeRotation(newRotation);
self.transform = transform;
[UIView commitAnimations];
The rotation should indeed take place around the center of the UIView. Setting the animationDuration to zero garantees no tweening should happen.
Be sure though you don't do this 60 times a second. It's very tempting to create game like animations with these tools. These kind of animations aren't exactly meant for a frame to frame based, "lots of sprites flying everywhere" game.
For that - and I've been down that road - the only way to go, is OpenGL.