I am creating a very simple particle system with CoreAnimation.
This is my cell:
UIImage *image = [UIImage imageNamed:#"spark"];
CAEmitterCell *cell = [CAEmitterCell emitterCell];
[cell setContents:(id)image.CGImage];
[cell setBirthRate:250.f];
[cell setScale:.25f];
[cell setColor:[UIColor colorWithRed:1.0 green:0.2 blue:0.1 alpha:0.5].CGColor];
[cell setLifetime:5.0f];
and layer:
CAEmitterLayer *emitterLayer = [CAEmitterLayer layer];
[emitterLayer setEmitterCells:#[cell]];
[emitterLayer setFrame:bounds];
[emitterLayer setRenderMode:kCAEmitterLayerAdditive];
[self.view.layer addSublayer:emitterLayer];
Now, I move the emitterLayer's position to wherever I'm touching:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
self.emitterLayer.emitterPosition = [[touches anyObject] locationInView:self.view];
}
However, the problem is, it doesn't emit the particles continuously. But rather, at regular intervals(dotted as oppose to a line):
I thought perhaps it's because I'm not animating it. So I tried to add a simple animation to move the emitter from top left to bottom right of the screen just to see if that works:
CGPoint startPos = CGPointZero;
CGPoint endPos = CGPointMake(bounds.size.width, bounds.size.height);
CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:#"emitterPosition"];
ba.fromValue = [NSValue valueWithCGPoint:startPos];
ba.toValue = [NSValue valueWithCGPoint:endPos];
ba.duration = .5f;
[emitterLayer addAnimation:ba forKey:nil];
What I expected is a line be be formed with particles emitted continuously. However, what I see particles being emitted at intervals, like this:
Is it possible to have the emitterLayer follow your touch up and continuously emit these particles like a drawing and not be dotted?
Thanks
The effect you are looking to achieve (I saw a link in a comment) is probably best achieved without a particle system. What you usually use for this effect depends on individual approach, but I've had success with both.
One, is to create a render buffer: You draw directly onto this buffer with a colour at maximum value. This could be a UIView for instance. At a timed interval, you decrease every pixel value in this view by a percentage (say 0.05%). New pixels added start bright and fade over time this way, so you have no gaps between points. This is CPU intensive!
The other is to create a 'trianglestrip': you will need to go into a lower level graphics API for this such as OpenGL. The idea is that you create a section of triangles (or quads) that form a 'ribbon' under your finger. Each new section that is added begins fading to 0% alpha and is then removed. There is an implementation of this technique in cocos2d which is called CCMotionStreak that is perfect for this. The benefit is that you supply a texture (as you would a particle system) so it keeps designers happy
I'd suggest removing the animation and to create the line effect via the CAEmitterCell's velocity and emissionLongitude properties:
UIImage *image = [UIImage imageNamed:#"spark"];
CAEmitterCell *cell = [CAEmitterCell emitterCell];
[cell setContents:(id)image.CGImage];
[cell setBirthRate:250.f];
[cell setScale:.25f];
[cell setColor:[UIColor colorWithRed:1.0 green:0.2 blue:0.1 alpha:0.5].CGColor];
[cell setLifetime:5.0f];
[cell setEmissionLongitude:M_PI_4]; //The emission angle
[cell setVelocity:500]; //adjust as needed, together with BirthRate
CAEmitterLayer *emitterLayer = [CAEmitterLayer layer];
[emitterLayer setEmitterCells:#[cell]];
[emitterLayer setFrame:bounds];
[emitterLayer setRenderMode:kCAEmitterLayerAdditive];
[self.view.layer addSublayer:emitterLayer];
That looks awfully like the pattern you'd see using an ease-in ease-out animation curve, which I think is the default for CAAnimations. The points are closer together at either end, suggesting it is slower at the beginning and end.
Change the timing function of the animation to linear and you should see the effect you want.
To get a continuous line of emitters the only thing I could get to work was to slow the animation down, which doesn't really give the right effect. It looks like moving the emitter position skips a few renders, I'm not sure why.
You also need to play with the scaling a bit and probably use a CAShapeLayer to draw the actual line you're after.
In fact, a better solution is probably to use a smaller emitter layer which you move along at the end of the line, with a CAShapeLayer which you animate the strokeEnd of to leave the drawing in there.
It seems to me like your birthRate is set way too low. Try changing it from 250.f to something like 1500.f and post the results.
Related
I am running into an issue when I create an explicit animation to change the value of a CAShapeLayer's path from an ellipse to a rect.
In my canvas controller I setup a basic CAShapeLayer and add it to the root view's layer:
CAShapeLayer *aLayer;
aLayer = [CAShapeLayer layer];
aLayer.frame = CGRectMake(100, 100, 100, 100);
aLayer.path = CGPathCreateWithEllipseInRect(aLayer.frame, nil);
aLayer.lineWidth = 10.0f;
aLayer.strokeColor = [UIColor blackColor].CGColor;
aLayer.fillColor = [UIColor clearColor].CGColor;
[self.view.layer addSublayer:aLayer];
Then, when I animate the path I get a strange glitch / flicker in the last few frames of the animation when the shape becomes a rect, and in the first few frames when it animates away from being a rect. The animation is set up as follows:
CGPathRef newPath = CGPathCreateWithRect(aLayer.frame, nil);
[CATransaction lock];
[CATransaction begin];
[CATransaction setAnimationDuration:5.0f];
CABasicAnimation *ba = [CABasicAnimation animationWithKeyPath:#"path"];
ba.autoreverses = YES;
ba.fillMode = kCAFillModeForwards;
ba.repeatCount = HUGE_VALF;
ba.fromValue = (id)aLayer.path;
ba.toValue = (__bridge id)newPath;
[aLayer addAnimation:ba forKey:#"animatePath"];
[CATransaction commit];
[CATransaction unlock];
I have tried many different things like locking / unlocking the CATransaction, playing with various fill modes, etc...
Here's an image of the glitch:
http://www.postfl.com/outgoing/renderingglitch.png
A video of what I am experiencing can be found here:
http://vimeo.com/37720876
I received this feedback from the quartz-dev list:
David Duncan wrote:
Animating the path of a shape layer is only guaranteed to work when
you are animating from like to like. A rectangle is a sequence of
lines, while an ellipse is a sequence of arcs (you can see the
sequence generated by using CGPathApply), and as such the animation
between them isn't guaranteed to look very good, or work well at all.
To do this, you basically have to create an analog of a rectangle by
using the same curves that you would use to create an ellipse, but
with parameters that would cause the rendering to look like a
rectangle. This shouldn't be too difficult (and again, you can use
what you get from CGPathApply on the path created with
CGPathAddEllipseInRect as a guide), but will likely require some
tweaking to get right.
Unfortunately this is a limitation of the otherwise awesome animatable path property of CAShapeLayers.
Basically it tries to interpolate between the two paths. It hits trouble when the destination path and start path have a different number of control points - and curves and straight edges will have this problem.
You can try to minimise the effect by drawing your ellipse as 4 curves instead of a single ellipse, but it still isn't quite right. I haven't found a way to go smoothly from curves to polygons.
You may be able to get most of the way there, then transfer to a fade animation for the last part - this won't look as nice, though.
Strange issue I can't seem to resolve where on iOS 7 only, CAEmitterLayer will spawn particles on the screen incorrectly when birth rate is initially set to a nonzero value. It's as if it calculates the state the layer would be in the future.
// Create black image particle
CGRect rect = CGRectMake(0, 0, 20, 20);
UIGraphicsBeginImageContext(rect.size);
CGContextFillRect(UIGraphicsGetCurrentContext(), rect);
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// Create cell
CAEmitterCell *cell = [CAEmitterCell emitterCell];
cell.contents = (__bridge id)img.CGImage;
cell.birthRate = 100.0;
cell.lifetime = 10.0;
cell.velocity = 100.0;
// Create emitter with particles emitting from a line on the
// bottom of the screen
CAEmitterLayer *emitter = [CAEmitterLayer layer];
emitter.emitterShape = kCAEmitterLayerLine;
emitter.emitterSize = CGSizeMake(self.view.bounds.size.width,0);
emitter.emitterPosition = CGPointMake(self.view.bounds.size.width/2,
self.view.bounds.size.height);
emitter.emitterCells = #[cell];
[self.view.layer addSublayer:emitter];
I saw on the DevForums one post where a few people mentioned they had similar problems with iOS 7 and CAEmitterLayer, but no one had any ideas how to fix it. Now that iOS 7 is no longer beta, I figured I should ask here and see if anyone can crack it. I really hope this isn't just a bug that we have to wait for 7.0.1 or 7.1 to get fixed. Any ideas would be much appreciated. Thanks!
YES!
I spent hours on this problem myself.
To get the same kind of animation of the birthRate we had before we use a couple of strategies.
Firstly, if you want the layer to look like it begins emitting when added to the view you need to remember that CAEmitterLayer is a subclass of CALayer which conforms to the CAMediaTiming protocol. We have to set the whole emitter layer to begin at the current moment:
emitter.beginTime = CACurrentMediaTime();
[self.view.layer addSublayer:emitter];
It's as if it calculates the state the layer would be in the future.
You were eerily close, but actually its that the emitter was beginning in the past.
Secondly, to animate between a birthrate of 0 and n, with the effect that we had before we can manipulate the lifetime property instead:
if (shouldBeEmitting){
emitter.lifetime = 1.0;
}
else{
emitter.lifetime = 0;
}
Note that i set the lifetime on the emitter layer itself. This is because when emitting the emitter cell's version of this property gets multiplied by the value in the emitter layer. Setting the lifetime of the emitter layer sets a multiple of the lifetimes of all your emitter cells, allowing you to turn them all on and off with ease.
For me, the issue with my CAEmitterLayer, when moving to iOS7 was the following:
In iOS7 setting the CAEmitterLayerCell's duration resulted in the particle not showing at all!
The only thing I had to change was remove the cell.duration = XXX and then my particles began showing up again. I am going to eat an Apple over this unexpected, unexplained hassle.
I've been having a problem that I can't seem to figure out. In my app I use a custom UIImageView class to represent movable objects. Some are loaded with static .png images and use frame animation, while others are loaded with CAShapeLayer paths from .svg files. For some of those, I'm getting stutter when the layer is animating from one path to another, while the UIImageView is moving.
You can see it in the linked video. When I touch the mermaid horn, a note spawns (svg path) and animates into a fish (another svg path), all while drifting upwards. The stuttering occurs during that animation / drift. It's most noticeable the third time I spawn the fish, around 19 seconds into the video. (The jumping at the end of each animation I need to fix separately so don't worry about that)
http://www.youtube.com/watch?v=lnrNWuvqQ4w
This does not occur when I test the app in iOS Simulator - everything is smooth. On my iPad 2 it stutters. When I turn off the note/fish movement (drifting upward), it animates smooth on the iPad, so obviously it has to do with moving the view at the same time. There's something I'm missing but I can't figure it out.
Here is how I set up the animation. PocketSVG is a class I found on Github that converts an .svg to a Bezier path.
animShape = [CAShapeLayer layer];
[animShape setShouldRasterize:YES];
[animShape setRasterizationScale:[[UIScreen mainScreen] scale]];
PocketSVG *tSVG;
UIBezierPath *tBezier;
PocketSVG *tSVG2;
UIBezierPath *tBezier2;
CAShapeLayer *tLayer2;
CABasicAnimation *animBezier;
CABasicAnimation *animScale;
tSVG = [[PocketSVG alloc] initFromSVGFileNamed:[NSString stringWithFormat:#"%#", tName]];
// Use PocketSVG to convert the SVG to a Bezier Path
tBezier = tSVG.bezier;
animShape.path = tBezier.CGPath;
animShape.lineWidth = 1;
animShape.strokeColor = [[UIColor blackColor] CGColor];
animShape.fillColor = [[UIColor blackColor] CGColor];
animShape.fillRule = kCAFillRuleNonZero;
// Set the frame & bounds to position the piece and set anchor point for rotation
// parentPaper is the Mermaid the note spawns from
CGPoint newCenter;
CGFloat imageScale = fabsf(parentPaper.transform.a);
newCenter = // Code excised, set center based on custom spawn points in relation to parent position and scale
animShape.bounds = CGPathGetBoundingBox(animShape.path);
CGRect lBounds = CGRectMake(newCenter.x, newCenter.y, animShape.bounds.size.width, animShape.bounds.size.height);
[animShape setFrame:lBounds];
halfSize = CGPointMake(animShape.bounds.size.width/2, animShape.bounds.size.height/2);
animShape.anchorPoint = CGPointMake(prp.childSpawnX, prp.childSpawnY);
// Create the end path for animation
tSVG2 = [[PocketSVG alloc] initFromSVGFileNamed:[NSString stringWithFormat:#"%#2", tName]];
tBezier2 = tSVG2.bezier;
tLayer2 = [CAShapeLayer layer];
tLayer2.path = tBezier2.CGPath;
// Create the animation and add it to the layer
animBezier = [CABasicAnimation animationWithKeyPath:#"path"];
animBezier.delegate = self;
animBezier.duration = prp.frameDur;
animBezier.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animBezier.fillMode = kCAFillModeForwards;
animBezier.removedOnCompletion = NO;
animBezier.autoreverses = behavior.autoReverse;
if (prp.animID < 0) {
animBezier.repeatCount = FLT_MAX;
}
else {
animBezier.repeatCount = prp.animID;
}
animBezier.fromValue = (id)animShape.path;
animBezier.toValue = (id)tLayer2.path;
[animShape addAnimation:animBezier forKey:#"animatePath"];
// also scale the animation if spawned from a parent
// i.e. small note to normal-sized fish
if (parentPaper != nil) {
animScale = [CABasicAnimation animationWithKeyPath:#"transform"];
animScale.delegate = self;
animScale.duration = prp.frameDur;
animScale.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animScale.fillMode = kCAFillModeForwards;
animScale.removedOnCompletion = YES;
animScale.autoreverses = NO;
animScale.repeatCount = 1;
animScale.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(imageScale, imageScale, 0.0)];
animScale.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.0, 1.0, 0.0)];
[animShape addAnimation:animScale forKey:#"animateScale"];
}
[self.layer addSublayer:animShape];
And this is basically how I'm moving the UIImageView. I use a CADisplayLink at 60 frames, figure out the new spot based on existing position of animShape.position, and update like this, where self is the UIImageView:
[self.animShape setPosition:paperCenter];
Everything else runs smooth, it's just this one set of objects that stutter and jump about when running on the iPad itself. Any ideas of what I'm doing wrong? Moving it the wrong way, perhaps? I do get confused with layers, frames and bounds still.
To fix this I ended up changing the way the UIImageView moves. Instead of changing the frame position through my game loop, I switched it to a CAKeyframeAnimation on the position since it's a temporary movement that only runs once. I used CGPathAddCurveToPoint to replicate the sine wave movement that I originally had. It was rather simple and I probably should have done it that way to begin with.
I can only surmise that the stuttering was due to moving it through the CADisplayLink loop while its animation separately.
I'm trying to reproduce some of the 2D transitions in Impress.js's sample presentation in Objective C. Specifically the rotate, pan, and scaling - right now I'm focusing on the scaling.
I've tried scaling a UILabel to the point where it "passes the screen", as the "visualize your Big thoughts" -> "and tiny ideas" does in the sample presentation.
This is what I've tried so far:
UILabel *label = [[UILabel alloc] init];
label.text = #"Hello World!";
label.textColor = [UIColor blackColor];
label.font = [UIFont fontWithName:#"Arial" size:18.f];
[label sizeToFit];
label.center = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2);
[self.view addSubview:label];
label.contentScaleFactor *= 80;
[UIView animateWithDuration:5 animations:^{
label.transform = CGAffineTransformScale(label.transform, 80, 80);
}];
Unfortunately this eats up about ~30-60 MB of RAM, depending on what the contentScaleFactor and initial font size is. If I don't increase the contentScaleFactor the text looks blurry. Increasing the font size also seems to eat just as much memory.
Here is what it looks in the profiler:
And this is just a single UILabel.
Is there any way to do this without eating up so much memory, without sacrificing quality of the text being rendered or the transitions?
Project download link
I don't believe it's necessary to leave Quartz to achieve this reproduction. Everything you've described as well as everything I've gathered from messing with Impress.js seems to be replicable through applying transforms (mostly 2D, some 3D) to a set of UILabels added to a container view that can be moved freely within the main view.
To do this, the project I created uses a subclass of UILabel titled "ImpressLabel" with an extra init function where instead of passing the label a frame, you pass it a size, a center point, and a CGFloat for the label's rotation on the Z axis. This transform is applied to the label upon instantiation so that when you set up the labels they will appear on screen already in the position and transformation you specify.
Then as far as configuring the text goes, you can pass the label an NSAttributedString instead of a NSString. This allows you to modify different parts of the string independently, so different words in the label can be different sizes, fonts, colors, background colors, etc. Here's an example of the above two paragraphs:
ImpressLabel *label1 = [[ImpressLabel alloc] initWithSize:CGSizeMake(260.0f, 80.0f) andCenterPointInSuperview:CGPointMake(500.0f, 500.0f) andRotationInSuperview:0.0f andEndingScaleFactor:1.3];
NSMutableAttributedString *firstLabelAttributes = [[NSMutableAttributedString alloc] initWithString:#"then you should try\nimpress.js*\n* no rhyme intended"];
[firstLabelAttributes addAttribute:NSFontAttributeName
value:[UIFont systemFontOfSize:label1.font.pointSize - 2]
range:NSMakeRange(0, 19)];
[firstLabelAttributes addAttribute:NSFontAttributeName
value:[UIFont systemFontOfSize:label1.font.pointSize - 8]
range:NSMakeRange(firstLabelAttributes.string.length - 19, 19)];
[firstLabelAttributes addAttribute:NSFontAttributeName
value:[UIFont boldSystemFontOfSize:label1.font.pointSize + 14]
range:NSMakeRange(23, 11)];
[label1 setNumberOfLines:3];
[label1 setAttributedText:firstLabelAttributes];
[label1 setTextAlignment:NSTextAlignmentCenter];
[containmentView addSubview:label1];
Now, on to more of the guts of the whole operation. As I mentioned above, this subclass adds a tap gesture to each label. When a tap is recognized a few things happen. The view containing the labels will pan/back/away by adjusting it's scale. It will also begin rotate and adjust its anchor point in the main view so that when the animation stops, the selected label will be centered on screen in the correct orientation. Then of course, while this is all going on the alpha of the selected label will be brought up to 1.0f while the alpha of the rest will be lowered to 0.25f.
- (void)tapForRotationDetected:(UITapGestureRecognizer *)sender {
CABasicAnimation *scale = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
[scale setToValue:[NSNumber numberWithFloat:0.8]];
[scale setAutoreverses:YES];
[scale setDuration:0.3];
//Create animation to adjust the container views anchorpoint.
CABasicAnimation *adjustAnchor = [CABasicAnimation animationWithKeyPath:#"anchorPoint"];
[adjustAnchor setFromValue:[NSValue valueWithCGPoint:self.superview.layer.anchorPoint]];
[adjustAnchor setToValue:[NSValue valueWithCGPoint:CGPointMake(self.center.x / self.superview.frame.size.width, self.center.y / self.superview.frame.size.height)]];
[adjustAnchor setRemovedOnCompletion:NO];
[adjustAnchor setFillMode:kCAFillModeForwards];
//Create animation to rotate the container view within its superview.
CABasicAnimation *rotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
//Create the animation group to apply these transforms
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
[animationGroup setAnimations:#[adjustAnchor,rotation]];
[animationGroup setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[animationGroup setDuration:0.6];
//Apply the end results of the animations directly to the container views layer.
[self.superview.layer setTransform:CATransform3DRotate(CATransform3DIdentity, DEGREES_TO_RADIANS(-self.rotationInSuperview), 0.0f, 0.0f, 1.0f)];
[self.superview.layer setAnchorPoint:CGPointMake(self.center.x / self.superview.frame.size.width, self.center.y / self.superview.frame.size.height)];
[self.superview.layer addAnimation:animationGroup forKey:#"animationGroup"];
//Animate the alpha property of all ImpressLabels in the container view.
[self.superview bringSubviewToFront:self];
[UIView animateWithDuration:0.4 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
for (ImpressLabel *label in sender.view.superview.subviews) {
if ([label isKindOfClass:[ImpressLabel class]]) {
if (label != self) {
[label setAlpha:0.25f];
}else{
[label setAlpha:1.0f];
}
}
}
} completion:nil];
}
Now, to address some of the concerns listed in your question.
I've profiled this project in Instruments' allocations tool and it
only consumes about 3.2 MB overall, so I'd say this approach is
efficient enough.
The sample I've provided animates most objects in 2D space, with the exception of the scaling animation which is an illusion at best. What I've done here is meant to serve as an example of how this can be done and isn't a 100% complete demonstration because as I stated above, animation really isn't my area of expertise. However, from looking over the docs, it seems like the key to having a label rotated in the third dimension and adjusting its superview to rotate all the labels and leave the selected label flat would be the use of CATransform3DInvert(). Although I haven't yet had time to fully figure out it works, it looks like it might be exactly what is needed in this scenario.
As far as mirroring goes, I don't believe there will be any problems with properly scaling everything. Looking over Apple's Multiple Display Programming Guide, it looks like the object passed to the NSNotification UIScreenDidConnectNotification is a UIScreen object. Since this is the case, you can easily ask for this displays bounds, and adjust the frames of the labels and the container view accordingly.
Note: In this example, only 0, 90, 180, and -90 degree transforms animate 100% correctly, due to the anchor point's coordinates being incorrectly generated. It looks like the solution lies with CGPointApplyAffineTransform(<#CGPoint point#>, <#CGAffineTransform t#>), but again I haven't had as much time to play with it as I would have liked. Either way, this should be plenty to get you started with your reproduction.
This has definitely sparked my interest though and when I get a chance to work on this again, I'll gladly update this post with any new information. Hope this helps!
I think it would help if you draw the text on a CALayer. A library that could help with this is: https://github.com/ZaBlanc/CanvasKit
Draw the next step of your animation on a new CALayer and transform to that. You could chain the required animations using this library: https://github.com/yangmeyer/CPAnimationSequence or this library: https://github.com/clayallsopp/Walt
I'm trying to create a UIView which shows a semitransparent circle with an opaque border inside its bounds. I want to be able to change the bounds in two ways - inside a -[UIView animateWithDuration:animations:] block and in a pinch gesture recogniser action which fires several times a second. I've tried three approaches based on answers elsewhere on SO, and none are suitable.
Setting the corner radius of the view's layer in layoutSubviews gives smooth translations, but the view doesn't stay circular during animations; it seems that cornerRadius isn't animatable.
Drawing the circle in drawRect: gives a consistently circular view, but if the circle gets too big then resizing in the pinch gesture gets choppy because the device is spending too much time redrawing the circle.
Adding a CAShapeLayer and setting its path property in layoutSublayersOfLayer, which doesn't animate inside UIView animations since path isn't implicitly animatable.
Is there a way for me to create a view which is consistently circular and smoothly resizable? Is there some other type of layer I could use to take advantage of the hardware acceleration?
UPDATE
A commenter has asked me to expand on what I mean when I say that I want to change the bounds inside a -[UIView animateWithDuration:animations:] block. In my code, I have a view which contains my circle view. The circle view (the version that uses cornerRadius) overrides -[setBounds:] in order to set the corner radius:
-(void)setBounds:(CGRect)bounds
{
self.layer.cornerRadius = fminf(bounds.size.width, bounds.size.height) / 2.0;
[super setBounds:bounds];
}
The bounds of the circle view are set in -[layoutSubviews]:
-(void)layoutSubviews
{
// some other layout is performed and circleRadius and circleCenter are
// calculated based on the properties and current size of the view.
self.circleView.bounds = CGRectMake(0, 0, circleRadius*2, circleRadius*2);
self.circleView.center = circleCenter;
}
The view is sometimes resized in animations, like so:
[UIView animateWithDuration:0.33 animations:^(void) {
myView.frame = CGRectMake(x, y, w, h);
[myView setNeedsLayout];
[myView layoutIfNeeded];
}];
but during these animations, if I draw the circle view using a layer with a cornerRadius, it goes funny shapes. I can't pass the animation duration in to layoutSubviews so I need to add the right animation within -[setBounds].
As the section on Animations in the "View Programming Guide for iOS" says
Both UIKit and Core Animation provide support for animations, but the level of support provided by each technology varies. In UIKit, animations are performed using UIView objects
The full list of properties that you can animate using either the older
[UIView beginAnimations:context:];
[UIView setAnimationDuration:];
// Change properties here...
[UIView commitAnimations];
or the newer
[UIView animateWithDuration:animations:];
(that you are using) are:
frame
bounds
center
transform (CGAffineTransform, not the CATransform3D)
alpha
backgroundColor
contentStretch
What confuses people is that you can also animate the same properties on the layer inside the UIView animation block, i.e. the frame, bounds, position, opacity, backgroundColor.
The same section goes on to say:
In places where you want to perform more sophisticated animations, or animations not supported by the UIView class, you can use Core Animation and the view’s underlying layer to create the animation. Because view and layer objects are intricately linked together, changes to a view’s layer affect the view itself.
A few lines down you can read the list of Core Animation animatable properties where you see this one:
The layer’s border (including whether the layer’s corners are rounded)
There are at least two good options for achieving the effect that you are after:
Animating the corner radius
Using a CAShapeLayer and animating the path
Both of these require that you do the animations with Core Animation. You can create a CAAnimationGroup and add an array of animations to it if you need multiple animations to run as one.
Update:
Fixing things with as few code changes as possible would be done by doing the corner radius animation on the layer at the "same time" as the other animations. I put quotations marks around same time since it is not guaranteed that animations that are not in the same group will finish at exactly the same time. Depending on what other animations you are doing it might be better to use only basic animations and animations groups. If you are applying changes to many different views in the same view animation block then maybe you could look into CATransactions.
The below code animates the frame and corner radius much like you describe.
UIView *circle = [[UIView alloc] initWithFrame:CGRectMake(30, 30, 100, 100)];
[[circle layer] setCornerRadius:50];
[[circle layer] setBorderColor:[[UIColor orangeColor] CGColor]];
[[circle layer] setBorderWidth:2.0];
[[circle layer] setBackgroundColor:[[[UIColor orangeColor] colorWithAlphaComponent:0.5] CGColor]];
[[self view] addSubview:circle];
CGFloat animationDuration = 4.0; // Your duration
CGFloat animationDelay = 3.0; // Your delay (if any)
CABasicAnimation *cornerRadiusAnimation = [CABasicAnimation animationWithKeyPath:#"cornerRadius"];
[cornerRadiusAnimation setFromValue:[NSNumber numberWithFloat:50.0]]; // The current value
[cornerRadiusAnimation setToValue:[NSNumber numberWithFloat:10.0]]; // The new value
[cornerRadiusAnimation setDuration:animationDuration];
[cornerRadiusAnimation setBeginTime:CACurrentMediaTime() + animationDelay];
// If your UIView animation uses a timing funcition then your basic animation needs the same one
[cornerRadiusAnimation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
// This will keep make the animation look as the "from" and "to" values before and after the animation
[cornerRadiusAnimation setFillMode:kCAFillModeBoth];
[[circle layer] addAnimation:cornerRadiusAnimation forKey:#"keepAsCircle"];
[[circle layer] setCornerRadius:10.0]; // Core Animation doesn't change the real value so we have to.
[UIView animateWithDuration:animationDuration
delay:animationDelay
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
[[circle layer] setFrame:CGRectMake(50, 50, 20, 20)]; // Arbitrary frame ...
// You other UIView animations in here...
} completion:^(BOOL finished) {
// Maybe you have your completion in here...
}];
With many thanks to David, this is the solution I found. In the end what turned out to be the key to it was using the view's -[actionForLayer:forKey:] method, since that's used inside UIView blocks instead of whatever the layer's -[actionForKey] returns.
#implementation SGBRoundView
-(CGFloat)radiusForBounds:(CGRect)bounds
{
return fminf(bounds.size.width, bounds.size.height) / 2;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
self.opaque = NO;
self.layer.backgroundColor = [[UIColor purpleColor] CGColor];
self.layer.borderColor = [[UIColor greenColor] CGColor];
self.layer.borderWidth = 3;
self.layer.cornerRadius = [self radiusForBounds:self.bounds];
}
return self;
}
-(void)setBounds:(CGRect)bounds
{
self.layer.cornerRadius = [self radiusForBounds:bounds];
[super setBounds:bounds];
}
-(id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
id<CAAction> action = [super actionForLayer:layer forKey:event];
if ([event isEqualToString:#"cornerRadius"])
{
CABasicAnimation *boundsAction = (CABasicAnimation *)[self actionForLayer:layer forKey:#"bounds"];
if ([boundsAction isKindOfClass:[CABasicAnimation class]] && [boundsAction.fromValue isKindOfClass:[NSValue class]])
{
CABasicAnimation *cornerRadiusAction = [CABasicAnimation animationWithKeyPath:#"cornerRadius"];
cornerRadiusAction.delegate = boundsAction.delegate;
cornerRadiusAction.duration = boundsAction.duration;
cornerRadiusAction.fillMode = boundsAction.fillMode;
cornerRadiusAction.timingFunction = boundsAction.timingFunction;
CGRect fromBounds = [(NSValue *)boundsAction.fromValue CGRectValue];
CGFloat fromRadius = [self radiusForBounds:fromBounds];
cornerRadiusAction.fromValue = [NSNumber numberWithFloat:fromRadius];
return cornerRadiusAction;
}
}
return action;
}
#end
By using the action that the view provides for the bounds, I was able to get the right duration, fill mode and timing function, and most importantly delegate - without that, the completion block of UIView animations didn't run.
The radius animation follows that of the bounds in almost all circumstances - there are a few edge cases that I'm trying to iron out, but it's basically there. It's also worth mentioning that the pinch gestures are still sometimes jerky - I guess even the accelerated drawing is still costly.
Starting in iOS 11, UIKit animates cornerRadius if you change it inside an animation block.
The path property of a CAShapeLayer isn't implicitly animatable, but it is animatable. It should be pretty easy to create a CABasicAnimation that changes the size of the circle path. Just makes sure that the path has the same number of control points (e.g. changing the radius of a full-circle arc.) If you change the number of control points, things get really strange. "Results are undefined", according to the documentaiton.