Animate an arc around centre? - ios

I'm porting some animation code that looks a bit like this:
- (void)drawRect:(CGRect)rect
{
self.angle += 0.1;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetRGBStrokeColor(context, 1.0, 0, 0, 1);
CGContextSetLineWidth(context, 2);
CGContextSetLineCap(context, kCGLineCapButt);
CGContextAddArc(context,
self.frame.size.height/2, self.frame.size.height/2, //center
self.frame.size.height/2 - 2, //radius
0.0 + self.angle, M_PI_4 + self.angle, //arc start/finish
NO);
CGContextStrokePath(context);
}
The problem is that drawRect is only ever called once, when the view is first drawn, so the position of the arc is never updated.
How can I achieve the effect I want (the arc slowly and continuously moving around the centre point)? Most of the animation examples I can find are to perform a one-time animation (such as a fade-In), but not something that is continuous.
I've also tried something along the lines of:
[arcView animateWithDuration:10.0f
delay:1.0f
options: UIViewAnimationOptionRepeat | UIViewAnimationOptionBeginFromCurrentState
animations: ^(void){
_arcView.transform = CGAffineTransformMakeRotation(self.angle++);
}
completion:NULL];
When showing the view, but this doesn't seem to be doing anything either.
A little more about what I'm aiming for: I have a View that I want to be able to set certain states on, e.g. arcView.state = STATE_READY, and for that to change the way it animates. This is being ported from an Android project where it's as simple as adding logic to the draw method on the View, and something reasonably analogous would be preferred.

A couple of observations:
First, drawRect should probably not be incrementing the angle. The purpose of drawRect is to draw a single frame and it should not be changing the state of the view.
If you wanted the UIView subclass to repeated update the angle and redraw itself, you would set up a timer, or better, a CADisplayLink, that will be called periodically, updating the angle and then calling [self setNeedsDisplay] to update the view:
#import "MyView.h"
#interface MyView ()
#property (nonatomic, strong) CADisplayLink *displayLink;
#property (nonatomic) CFTimeInterval startTime;
#property (nonatomic) CGFloat revolutionsPerSecond;
#property (nonatomic) CGFloat angle;
#end
#implementation MyView
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
self.angle = 0.0;
self.revolutionsPerSecond = 0.25; // i.e. go around once per every 4 seconds
[self startDisplayLink];
}
return self;
}
- (void)startDisplayLink
{
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
self.startTime = CACurrentMediaTime();
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopDisplayLink
{
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink
{
CFTimeInterval elapsed = CACurrentMediaTime() - self.startTime;
double revolutions;
double percent = modf(elapsed * self.revolutionsPerSecond, &revolutions);
self.angle = M_PI * 2.0 * percent;
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetRGBStrokeColor(context, 1.0, 0, 0, 1);
CGContextSetLineWidth(context, 2);
CGContextSetLineCap(context, kCGLineCapButt);
CGContextAddArc(context,
self.frame.size.width/2, self.frame.size.height/2, //center
MIN(self.frame.size.width, self.frame.size.height) / 2.0 - 2.0, //radius
0.0 + self.angle, M_PI_4 + self.angle, //arc start/finish
NO);
CGContextStrokePath(context);
}
#end
An easier approach is to update the transform property of the view, to rotate it. In iOS 7 and later, you add a view with the arc and then rotate it with something like:
[UIView animateKeyframesWithDuration:4.0 delay:0.0 options:UIViewKeyframeAnimationOptionRepeat | UIViewAnimationOptionCurveLinear animations:^{
[UIView addKeyframeWithRelativeStartTime:0.00 relativeDuration:0.25 animations:^{
self.view.transform = CGAffineTransformMakeRotation(M_PI_2);
}];
[UIView addKeyframeWithRelativeStartTime:0.25 relativeDuration:0.25 animations:^{
self.view.transform = CGAffineTransformMakeRotation(M_PI);
}];
[UIView addKeyframeWithRelativeStartTime:0.50 relativeDuration:0.25 animations:^{
self.view.transform = CGAffineTransformMakeRotation(M_PI_2 * 3.0);
}];
[UIView addKeyframeWithRelativeStartTime:0.75 relativeDuration:0.25 animations:^{
self.view.transform = CGAffineTransformMakeRotation(M_PI * 2.0);
}];
} completion:nil];
Note, I've broken the animation up into four steps, because if you rotate from 0 to M_PI * 2.0, it won't animate because it knows the ending point is the same as the starting point. So by breaking it up into four steps like that it does each of the four animations in succession. If doing in in earlier versions of iOS, you do something similar with animateWithDuration, but to have it repeat, you need the completion block to invoke another rotation. Something like: https://stackoverflow.com/a/19408039/1271826
Finally, if you want to animate, for example, just the end of the arc (i.e. to animate the drawing of the arc), you can use CABasicAnimation with a CAShapeLayer:
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.view.bounds.size.width / 2.0, self.view.bounds.size.height / 2.0) radius:MIN(self.view.bounds.size.width, self.view.bounds.size.height) / 2.0 - 2.0 startAngle:0 endAngle:M_PI_4 / 2.0 clockwise:YES];
CAShapeLayer *layer = [CAShapeLayer layer];
layer.frame = self.view.bounds;
layer.path = path.CGPath;
layer.lineWidth = 2.0;
layer.strokeColor = [UIColor redColor].CGColor;
layer.fillColor = [UIColor clearColor].CGColor;
[self.view.layer addSublayer:layer];
CABasicAnimation *animateStrokeEnd = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animateStrokeEnd.duration = 10.0;
animateStrokeEnd.fromValue = #0.0;
animateStrokeEnd.toValue = #1.0;
animateStrokeEnd.repeatCount = HUGE_VALF;
[layer addAnimation:animateStrokeEnd forKey:#"strokeEndAnimation"];
I know that's probably not what you're looking for, but I mention it just so you can add it to your animation "tool belt".

Most of the animation examples I can find are to perform a one-time
animation (such as a fade-In), but not something that is continuous.
If you use a method like UIView's +animateWithDuration:delay:options:animations:completion: to do your animation, you can specify UIViewAnimationOptionRepeat in the options parameter to make the animation repeat indefinitely.
Fundamentally, using -drawRect: for most animations is the wrong way to go. As you discovered, -drawRect: is only called when the graphics system really needs to redraw the view, and that's a relatively expensive process. As much as possible, you should use Core Animation to do your animations, especially for stuff like this where the view itself doesn't need to be redrawn, but it's pixels need to be transformed somehow.
Note that +animateWithDuration:delay:options:animations:completion: is a class method, so you should send it to UIView itself (or some subclass of UIView), not to an instance of UIView. There's an example here. Also, that particular method might or might not be the best choice for what you're doing -- I just called it out because it's an easy example of using animation with views and it lets you specify the repeating option.
I'm not sure what's wrong with your animation (other than maybe the wrong receiver as described above), but I'd avoid using the ++ operator to modify the angle. The angle is specified in radians, so incrementing by 1 probably isn't what you want. Instead, try adding π/2 to the current rotation, like this:
_arcView.transform = CAAffineTransformRotate(_arcView.transform, M_PI_2);

So this is what I've ended up with. It took a long time to work out the I needed "translation.rotation" instead of just "rotation"...
#implementation arcView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
self.state = 0;
if (self) {
self.layer.contents = (__bridge id)([[self generateArc:[UIColor redColor].CGColor]CGImage]);
}
return self;
}
-(UIImage*)generateArc:(CGColorRef)color{
UIGraphicsBeginImageContext(self.frame.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, color);
CGContextSetLineWidth(context, 2);
CGContextSetLineCap(context, kCGLineCapButt);
CGContextAddArc(context,
self.frame.size.height/2, self.frame.size.height/2,
self.frame.size.height/2 - 2,0.0,M_PI_4,NO);
CGContextStrokePath(context);
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}
-(void)spin{
[self.layer removeAnimationForKey:#"rotation"];
CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
NSNumber *current =[[self.layer presentationLayer] valueForKeyPath:#"transform.rotation"];
rotate.fromValue = current;
rotate.toValue = [NSNumber numberWithFloat:current.floatValue + M_PI * (self.state*2 - 1)];
rotate.duration = 3.0;
rotate.repeatCount = INT_MAX;
rotate.fillMode = kCAFillModeForwards;
rotate.removedOnCompletion = NO;
[self.layer addAnimation:rotate forKey:#"rotation"];
}

Related

Animate path of shape layer

I'm stumped by what I thought would be a simple problem.
I'd like to draw views connected by lines, animate the position of the views and have the connecting line animate too. I create the views, and create a line between them like this:
- (UIBezierPath *)pathFrom:(CGPoint)pointA to:(CGPoint)pointB {
CGFloat halfY = pointA.y + 0.5*(pointB.y - pointA.y);
UIBezierPath *linePath=[UIBezierPath bezierPath];
[linePath moveToPoint: pointA];
[linePath addLineToPoint:CGPointMake(pointA.x, halfY)];
[linePath addLineToPoint:CGPointMake(pointB.x, halfY)];
[linePath addLineToPoint:pointB];
return linePath;
}
-(void)makeTheLine {
CGPoint pointA = self.viewA.center;
CGPoint pointB = self.viewB.center;
CAShapeLayer *lineShape = [CAShapeLayer layer];
UIBezierPath *linePath=[self pathFrom:pointA to:pointB];
lineShape.path=linePath.CGPath;
lineShape.fillColor = nil;
lineShape.opacity = 1.0;
lineShape.strokeColor = [UIColor blackColor].CGColor;
[self.view.layer addSublayer:lineShape];
self.lineShape = lineShape;
}
It draws just how I want it to. My understanding from the docs is that I am allowed to animate a shape's path by altering it in an animation block, like this:
- (void)moveViewATo:(CGPoint)dest {
UIBezierPath *destPath=[self pathFrom:dest to:self.viewB.center];
[UIView animateWithDuration:1 animations:^{
self.viewA.center = dest;
self.lineShape.path = destPath.CGPath;
}];
}
But no dice. The view position animates as expected, but the line connecting to the other view "jumps" right away to the target path.
This answer implies that what I'm doing should work. And this answer suggests a CABasic animation, which seems worse to me since (a) I'd then need to coordinate with the much cooler block animation done to the view, and (b) when I tried it this way, the line didn't change at all....
// worse way
- (void)moveViewATo:(CGPoint)dest {
UIBezierPath *linePath=[self pathFrom:dest to:self.viewB.center];
[UIView animateWithDuration:1 animations:^{
self.viewA.center = dest;
//self.lineShape.path = linePath.CGPath;
}];
CABasicAnimation *morph = [CABasicAnimation animationWithKeyPath:#"path"];
morph.duration = 1;
morph.toValue = (id)linePath.CGPath;
[self.view.layer addAnimation:morph forKey:nil];
}
Thanks in advance.
Thanks all for the help. What I discovered subsequent to asking this is that I was animating the wrong property. It turns out, you can replace the layer's shape in an animation, like this:
CABasicAnimation *morph = [CABasicAnimation animationWithKeyPath:#"path"];
morph.duration = 1;
morph.fromValue = (__bridge id)oldPath.path;
morph.toValue = (__bridge id)newPath.CGPath;
[line addAnimation:morph forKey:#"change line path"];
line.path=linePath.CGPath;
I guess this is all you need:
#import "ViewController.h"
#interface ViewController ()
//the view to animate, nothing but a simple empty UIView here.
#property (nonatomic, strong) IBOutlet UIView *targetView;
#property (nonatomic, strong) CAShapeLayer *shapeLayer;
#property NSTimeInterval animationDuration;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//the shape layer appearance
self.shapeLayer = [[CAShapeLayer alloc]init];
self.shapeLayer.strokeColor = [UIColor blackColor].CGColor;
self.shapeLayer.fillColor = [UIColor clearColor].CGColor;
self.shapeLayer.opacity = 1.0;
self.shapeLayer.lineWidth = 2.0;
[self.view.layer insertSublayer:self.shapeLayer below:self.targetView.layer];
//animation config
self.animationDuration = 2;
}
- (UIBezierPath *)pathFrom:(CGPoint)pointA to:(CGPoint)pointB {
CGFloat halfY = pointA.y + 0.5*(pointB.y - pointA.y);
UIBezierPath *linePath=[UIBezierPath bezierPath];
[linePath moveToPoint: pointA];
[linePath addLineToPoint:CGPointMake(pointA.x, halfY)];
[linePath addLineToPoint:CGPointMake(pointB.x, halfY)];
[linePath addLineToPoint:pointB];
return linePath;
}
- (void) moveViewTo: (CGPoint) point {
UIBezierPath *linePath= [self pathFrom:self.targetView.center to:point];
self.shapeLayer.path = linePath.CGPath;
//Use CAKeyframeAnimation to animate the view along the path
//animate the position of targetView.layer instead of the center of targetView
CAKeyframeAnimation *viewMovingAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
viewMovingAnimation.duration = self.animationDuration;
viewMovingAnimation.path = linePath.CGPath;
//set the calculationMode to kCAAnimationPaced to make the movement in a constant speed
viewMovingAnimation.calculationMode =kCAAnimationPaced;
[self.targetView.layer addAnimation:viewMovingAnimation forKey:viewMovingAnimation.keyPath];
//draw the path, animate the keyPath "strokeEnd"
CABasicAnimation *lineDrawingAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
lineDrawingAnimation.duration = self.animationDuration;
lineDrawingAnimation.fromValue = [NSNumber numberWithDouble: 0];
lineDrawingAnimation.toValue = [NSNumber numberWithDouble: 1];
[self.shapeLayer addAnimation:lineDrawingAnimation forKey:lineDrawingAnimation.keyPath];
//This part is crucial, update the values, otherwise it will back to its original state
self.shapeLayer.strokeEnd = 1.0;
self.targetView.center = point;
}
//the IBAction for a UITapGestureRecognizer
- (IBAction) viewDidTapped:(id)sender {
//move the view to the tapped location
[self moveViewTo:[sender locationInView: self.view]];
}
#end
Some explanation:
For UIViewAnimation, the property value is changed when the
animation is completed. For CALayerAnimation, the property value is
never change, it is just an animation and when the animation is
completed, the layer will go to its original state (in this case, the
path).
Putting self.lineShape.path = linePath.CGPath doesn't work is
because self.linePath is a CALayer instead of a UIView, you
have to use CALayerAnimation to animate a CALayer
To draw a path, it's better to animate the path drawing with keyPath
strokeEnd instead of path. I'm not sure why path worked in the
original post, but it seems weird to me.
CAKeyframeAnimation (instead of CABasicAnimation or UIViewAnimation) is used to animate the view along the path. (I guess you would prefer this to the linear animation directly from start point to end point). Setting calculationMode to kCAAnimationPaced will give a constant speed to the animation, otherwise the view moving will not sync with the line drawing.

Circular loading animation (like Weather Channel App)

How would I implement a loading animation like the one found in the Weather Channel iOS App?
Specifically, I have a rounded UIButton that I want a spinning circle around when the user has tapped it, and something is loading.
Preferably, I'd want to make this using a built-in iOS framework, and as few 3rd party libraries as possible.
Here's how the Weather Channel Application looks like (couldn't get a gif, so to see it in action download the application from the App Store):
It doesn't necessarily have to look exactly like this, a solid color would be a good start. But the concept should be the same. I don't believe it should be hard to make, but sadly I have no idea where to start.
Thanks for all help!
EDIT 1:
I should point out that a solid color is good enough, and it doesn't need a gradient or a glow.
I have been working on some simple code that might be in the right direction. Please feel free to use that as a starting point:
- (void)drawRect:(CGRect)rect {
// Make the button round
self.layer.cornerRadius = self.frame.size.width / 2.0;
self.clipsToBounds = YES;
// Create the arc
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2) radius:(self.frame.size.width / 2) startAngle:M_PI_2 endAngle:M_PI clockwise:NO].CGPath;
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor redColor].CGColor;
circle.lineWidth = 10;
// Create the animation
// Add arc to button
[self.layer addSublayer:circle];
}
As pointed out elsewhere, you can just add a view, and the rotate it. If you want to rotate with block-based animation, it might look like:
- (void)rotateView:(UIView *)view {
[UIView animateWithDuration:1.0 delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
[UIView animateKeyframesWithDuration:1.0 delay:0.0 options:UIViewKeyframeAnimationOptionRepeat animations:^{
for (NSInteger i = 0; i < 4; i++) {
[UIView addKeyframeWithRelativeStartTime:(CGFloat) i / 4.0 relativeDuration:0.25 animations:^{
view.transform = CGAffineTransformMakeRotation((CGFloat) (i + 1) * M_PI / 2.0);
}];
}
} completion:nil];
} completion:nil];
}
Frankly, while I generally prefer block-based animation, the silliness of wrapping an animation with an animation (which is necessary to get no ease-in/ease-out) makes the block-based approach less compelling. But it illustrates the idea.
Alternatively, in iOS 7 and later, you could use UIKit Dynamics to spin it:
Define a property for the animator:
#property (nonatomic, strong) UIDynamicAnimator *animator;
Instantiate the animator:
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
Add rotation to an item:
UIDynamicItemBehavior *rotationBehavior = [[UIDynamicItemBehavior alloc] initWithItems:#[self.imageView]];
[rotationBehavior addAngularVelocity:2.0 forItem:self.imageView];
[self.animator addBehavior:rotationBehavior];
Maybe I miss something but I think you only need to rotate a custom spin asset.
You can easily achieve this with a method like:
- (void) runSpinAnimationOnView:(UIView*)view duration:(CGFloat)duration repeat:(float)repeat;
{
CABasicAnimation* rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2.0 * duration ];
rotationAnimation.duration = duration;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = repeat;
[view.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
}
You need to repeat it indefinitely. So put HUGE_VALF as repeat parameter.
And if you want to create a custom spin image according to your button size, you can use bezier path. For example, you can do something like:
-(UIImageView*)drawBezierPathInView:(UIView*)view{
UIGraphicsBeginImageContext(CGSizeMake(view.frame.size.width, view.frame.size.height));
//this gets the graphic context
CGContextRef context = UIGraphicsGetCurrentContext();
//you can stroke and/or fill
CGContextSetStrokeColorWithColor(context, [UIColor colorWithRed:0.5f green:0.5f blue:0.5f alpha:1.f].CGColor);
CGFloat lineWidth = 8.f;
UIBezierPath *spin = [UIBezierPath bezierPath];
[spin addArcWithCenter:CGPointMake(view.frame.size.width * 0.5f, view.frame.size.height * 0.5f) radius:view.frame.size.width * 0.5f - lineWidth * 0.5f startAngle:-M_PI_2 endAngle:2 * M_PI_2 clockwise:YES];
[spin setLineWidth:lineWidth];
[spin stroke];
//now get the image from the context
UIImage *bezierImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageView *bezierImageView = [[UIImageView alloc]initWithImage:bezierImage];
return bezierImageView;
}
And you create your button like:
// myButton is your custom button. spinView is an UIImageView, you keep reference on it to apply or to stop animation
self.spinView = [self drawBezierPathInView:mybutton];
[mybutton addSubview:self.spinView];
[self runSpinAnimationOnView:self.spinView duration:3 repeat:HUGE_VALF];

How do i build 3D transform in iOS for ECSlidingViewController?

I want to build a some special animation based https://github.com/ECSlidingViewController/ECSlidingViewController.
The Zoom animation is like below image.
I just want to rotate the main view controller by PI / 4. Like below image.
I had tried to EndState transform like below code, but it doesn't work.
- (void)topViewAnchorRightEndState:(UIView *)topView anchoredFrame:(CGRect)anchoredFrame {
CATransform3D toViewRotationPerspectiveTrans = CATransform3DIdentity;
toViewRotationPerspectiveTrans.m34 = -0.003;
toViewRotationPerspectiveTrans = CATransform3DRotate(toViewRotationPerspectiveTrans, M_PI_4, 0.0f, -1.0f, 0.0f);
topView.layer.transform = toViewRotationPerspectiveTrans;
topView.layer.position = CGPointMake(anchoredFrame.origin.x + ((topView.layer.bounds.size.width * MEZoomAnimationScaleFactor) / 2), topView.layer.position.y);
}
Any help, pointers or example code snippets would be really appreciated!
I managed to do it. From zoom animation code given in ECSlidingViewController, do not apply zoom factor in
- (CGRect)topViewAnchoredRightFrame:(ECSlidingViewController *)slidingViewController{
CGRect frame = slidingViewController.view.bounds;
frame.origin.x = slidingViewController.anchorRightRevealAmount;
frame.size.width = frame.size.width/* * MEZoomAnimationScaleFactor*/;
frame.size.height = frame.size.height/* * MEZoomAnimationScaleFactor*/;
frame.origin.y = (slidingViewController.view.bounds.size.height - frame.size.height) / 2;
return frame;
}
by commenting MEZoomAnimationScaleFactor.
Then at the top of - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext method add
[topView.layer setAnchorPoint:CGPointMake(0, 0.5)];
[topView.layer setZPosition:100];
Finally the method doing all the transformation must be :
- (void)topViewAnchorRightEndState:(UIView *)topView anchoredFrame:(CGRect)anchoredFrame {
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -0.003;
transform = CATransform3DScale(transform, ARDXZoomAnimationScaleFactor, ARDXZoomAnimationScaleFactor, 1);
transform = CATransform3DRotate(transform, -M_PI_4, 0.0, 1.0, 0.0);
topView.layer.transform = transform;
topView.frame = anchoredFrame;
topView.layer.position = CGPointMake(anchoredFrame.origin.x, anchoredFrame.size.height/2+anchoredFrame.origin.y);
}
Enjoy your animation :)
On top of ryancrunchi's answer, you also need to modify the topViewStartingState to reset the x position to 0 and not the middle of the container frame. This removes the jerky offset at the start of the animations:
Change
- (void)topViewStartingState:(UIView *)topView containerFrame:(CGRect)containerFrame {
topView.layer.transform = CATransform3DIdentity;
topView.layer.position = CGPointMake(containerFrame.size.width / 2, containerFrame.size.height / 2);
}
to
- (void)topViewStartingState:(UIView *)topView containerFrame:(CGRect)containerFrame {
topView.layer.transform = CATransform3DIdentity;
topView.layer.position = CGPointMake(0, containerFrame.size.height / 2);
}
There is also a mistake in the animation that causes a jerky left menu at the end of the opening animation. Copy the following line from the completion part of the animation so that it runs during the animation:
[self topViewAnchorRightEndState:topView anchoredFrame:[transitionContext finalFrameForViewController:topViewController]];
The animation function will now look like:
...
if (self.operation == ECSlidingViewControllerOperationAnchorRight) {
[containerView insertSubview:underLeftViewController.view belowSubview:topView];
[topView.layer setAnchorPoint:CGPointMake(0, 0.5)];
[topView.layer setZPosition:100];
[self topViewStartingState:topView containerFrame:containerView.bounds];
[self underLeftViewStartingState:underLeftViewController.view containerFrame:containerView.bounds];
NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration animations:^{
[self underLeftViewEndState:underLeftViewController.view];
underLeftViewController.view.frame = [transitionContext finalFrameForViewController:underLeftViewController];
[self topViewAnchorRightEndState:topView anchoredFrame:[transitionContext finalFrameForViewController:topViewController]];
} completion:^(BOOL finished) {
if ([transitionContext transitionWasCancelled]) {
underLeftViewController.view.frame = [transitionContext finalFrameForViewController:underLeftViewController];
underLeftViewController.view.alpha = 1;
[self topViewStartingState:topView containerFrame:containerView.bounds];
}
[transitionContext completeTransition:finished];
}];
}
...

iPhone: Animate circle with UIKit

I am using a CircleView class that basically inherits off UIView and implements drawRect to draw a circle. This all works, hurrah!
What I cannot figure out though is how to make it so when I touch it (touch code is implemented) the circle grows or pops. Normally I'd use the UIKit animation framework to do this but given I am basically overriding the drawRect function to directly draw the circle. So how do I animate this?
- (void)drawRect:(CGRect)rect{
CGContextRef context= UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, _Color.CGColor);
CGContextFillEllipseInRect(context, CGRectMake(0, 0, self.frame.size.width, self.frame.size.height));
}
- (void)handleSingleTap:(UITapGestureRecognizer *)recognizer {
// Animate?
}
The answers depends on what you mean by "grows or pops". When I hear "pop" I assume that the view scales up over a short period of time ans scales back down again to the original size. Something that "grows" on the other hand would scale up but not down again.
For something that scales up and down again over a short period of time I would use a transform to scale it. Custom drawing or not, UIView has build in support for animating a simple transform. If this is what you are looking for then it's not more then a few lines of code.
[UIView animateWithDuration:0.3
delay:0.0
options:UIViewAnimationOptionAutoreverse // reverse back to original value
animations:^{
// scale up 10%
yourCircleView.transform = CGAffineTransformMakeScale(1.1, 1.1);
} completion:^(BOOL finished) {
// restore the non-scaled state
yourCircleView.transform = CGAffineTransformIdentity;
}];
If on the other hand you want the circle to grow a little bit more on every tap then this won't do it for you since the view is going to look pixelated when it scales up. Making custom animations can be tricky so I would still advice you to use a scaling transform for the actual animation and then redraw the view after the animation.
[UIView animateWithDuration:0.3
animations:^{
// scale up 10%
yourCircleView.transform = CGAffineTransformMakeScale(1.1, 1.1);
} completion:^(BOOL finished) {
// restore the non-scaled state
yourCircleView.transform = CGAffineTransformIdentity;
// redraw with new value
yourCircleView.radius = theBiggerRadius;
}];
If you really, really want to do a completely custom animation then I would recommend that you watch Rob Napiers talk on Animating Custom Layer Properties, his example is even the exact thing you are doing (growing a circle).
If you want an animation that expands the ellipse from the centre, try this. In the header, define 3 variables:
BOOL _isAnimating;
float _time;
CGRect _ellipseFrame;
Then implement these 3 methods in the .m file:
- (void)drawRect:(CGRect)rect; {
[super drawRect:rect];
CGContextRef context= UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, _Color.CGColor);
CGContextFillEllipseInRect(context, _ellipseFrame);
}
- (void)expandOutward; {
if(_isAnimating){
_time += 1.0f / 30.0f;
if(_time >= 1.0f){
_ellipseFrame = self.frame;
_isAnimating = NO;
}
else{
_ellipseFrame = CGRectMake(0.0f, 0.0f, self.frame.size.width * _time, self.frame.size.height * _time);
_ellipseFrame.center = CGPointMake(self.frame.size.width / 2.0f, self.frame.size.height / 2.0f);
[self setNeedsDisplay];
[self performSelector:#selector(expandOutward) withObject:nil afterDelay:(1.0f / 30.0f)];
}
}
}
- (void)handleSingleTap:(UITapGestureRecognizer *)recognizer; {
if(_isAnimating == NO){
_time = 0.0f;
_isAnimating = YES;
[self expandOutward];
}
}
This is the most basic way you can animate the circle expanding from the centre. Look into CADisplayLink for a constant sync to the screen if you want more detailed animations. Hope that Helps!

CAShapeLayer animation (arc from 0 to final size)

I have a CAShapeLayer in which an arc is added using UIBezierPath. I saw a couple of posts (one actually) here on stackoverflow but none seemed to give me the answer. As said in the title I would like to animate an arc (yes it will be for a pie chart).
How can one accomplish an animation from an "empty" arc to the fully extended one? Kinda like a curved progress bar. Is it really impossible to do it via CoreAnimation?
Here's how I "do" the arc. Oh and ignore the comments and calculation since I'm used to counter clockwise unit circle and apple goes clockwise. The arcs appear just fine, I only need animation:
//Apple's unit circle goes in clockwise rotation while im used to counter clockwise that's why my angles are mirrored along x-axis
workAngle = 6.283185307 - DEGREES_TO_RADIANS(360 * item.value / total);
//My unit circle, that's why negative currentAngle
[path moveToPoint:CGPointMake(center.x + cosf(outerPaddingAngle - currentAngle) * (bounds.size.width / 2), center.y - (sinf(outerPaddingAngle - currentAngle) * (bounds.size.height / 2)))];
//Apple's unit circle, clockwise rotation, we add the angles
[path addArcWithCenter:center radius:120 startAngle:currentAngle endAngle:currentAngle + workAngle clockwise:NO];
//My unit circle, counter clockwise rotation, we subtract the angles
[path addLineToPoint:CGPointMake(center.x + cosf(-currentAngle - workAngle) * ((bounds.size.width / 2) - pieWidth), center.y - sinf(-currentAngle - workAngle) * ((bounds.size.height / 2) - pieWidth))];
//Apple's unit circle, clockwise rotation, addition of angles
[path addArcWithCenter:center radius:120 - pieWidth startAngle:currentAngle + workAngle endAngle:currentAngle - innerPaddingAngle clockwise:YES];
//No need for my custom calculations since I can simply close the path
[path closePath];
shape = [CAShapeLayer layer];
shape.frame = self.bounds;
shape.path = path.CGPath;
shape.fillColor = kRGBAColor(255, 255, 255, 0.2f + 0.1f * (i + 1)).CGColor; //kRGBAColor is a #defined
[self.layer addSublayer:shape];
//THIS IS THE PART IM INTERESTED IN
if (_shouldAnimate)
{
CABasicAnimation *pieAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
pieAnimation.duration = 1.0;
pieAnimation.removedOnCompletion = NO;
pieAnimation.fillMode = kCAFillModeForwards;
//from and to are just dummies so I dont get errors
pieAnimation.fromValue = [NSNumber numberWithInt:0]; //from what
pieAnimation.toValue = [NSNumber numberWithFloat:1.0f]; //to what
[shape addAnimation:pieAnimation forKey:#"animatePath"];
}
Here is an implementation of a CALayer based on CAShapeLayer:
ProgressLayer.h/m
#import <QuartzCore/QuartzCore.h>
#interface ProgressLayer : CAShapeLayer
-(void) computePath:(CGRect)r;
-(void) showProgress;
#end
#import "ProgressLayer.h"
#implementation ProgressLayer
-(id)init {
self=[super init];
if (self) {
self.path = CGPathCreateWithEllipseInRect(CGRectMake(0, 0, 50, 50), 0);
self.strokeColor = [UIColor greenColor].CGColor;
self.lineWidth=40;
self.lineCap = kCALineCapRound;
self.strokeEnd=0.001;
}
return self;
}
-(void) computePath:(CGRect)r {
self.path = CGPathCreateWithEllipseInRect(r, 0);
}
-(void)showProgress {
float advance=0.1;
if (self.strokeEnd>1) self.strokeEnd=0;
CABasicAnimation * swipe = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
swipe.duration=0.25;
swipe.fromValue=[NSNumber numberWithDouble:self.strokeEnd];
swipe.toValue= [NSNumber numberWithDouble:self.strokeEnd + advance];
swipe.fillMode = kCAFillModeForwards;
swipe.timingFunction= [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
swipe.removedOnCompletion=NO;
self.strokeEnd = self.strokeEnd + advance;
[self addAnimation:swipe forKey:#"strokeEnd animation"];
}
#end
I used this layer as a backing layer of an UIView:
#import "ProgressView.h"
#import "ProgressLayer.h"
#implementation ProgressView
-(id)initWithCoder:(NSCoder *)aDecoder {
self=[super initWithCoder:aDecoder];
if (self) {
[(ProgressLayer *)self.layer computePath:self.bounds];
}
return self;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
ProgressLayer * l = self.layer;
[l showProgress];
}
- (void)drawRect:(CGRect)rect {
[[UIColor redColor] setStroke];
UIRectFrame(self.bounds);
}
+(Class)layerClass {
return [ProgressLayer class];
}
#end
Before you create your animation you want to set your shape's strokeEnd to 0.
Then, use the key #"strokeEnd" instead of #"path"... Keys for CAAnimations are generally the name of the property you want to animate.
Then you can do the following:
[CATransaction begin]
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = 1.0f;
animation.removedOnCompletion = NO;
animation.fromValue = #0; //shorthand for creating an NSNumber
animation.toValue = #1; //shorthand for creating an NSNumber
[self addAnimation:animation forKey:#"animateStrokeEnd"];
[CATransaction commit];
You have to use the proper keys. "path" and "animatePath" don't mean anything to it - you have to use "strokeEnd" for your key/keypath. If you need to fiddle with your fill color, you use fillColor. The full list is here:
https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CAShapeLayer_class/Reference/Reference.html

Resources